From 025a1b01758662fed033380d3ae69149d2966e0c Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 10 May 2017 09:46:50 +0100 Subject: Enabled no-one as a merge access level in protected branches Closes #31541 --- app/models/concerns/protected_branch_access.rb | 18 ++++++++++++++++++ app/models/protected_branch/merge_access_level.rb | 10 ---------- app/models/protected_branch/push_access_level.rb | 18 ------------------ .../unreleased/protected-branches-no-one-merge.yml | 4 ++++ .../protected_branches/access_control_ce_spec.rb | 2 +- 5 files changed, 23 insertions(+), 29 deletions(-) create mode 100644 changelogs/unreleased/protected-branches-no-one-merge.yml diff --git a/app/models/concerns/protected_branch_access.rb b/app/models/concerns/protected_branch_access.rb index c41b807df8a..565d6a48192 100644 --- a/app/models/concerns/protected_branch_access.rb +++ b/app/models/concerns/protected_branch_access.rb @@ -7,5 +7,23 @@ module ProtectedBranchAccess belongs_to :protected_branch delegate :project, to: :protected_branch + + validates :access_level, presence: true, inclusion: { in: [Gitlab::Access::MASTER, + Gitlab::Access::DEVELOPER, + Gitlab::Access::NO_ACCESS] } + + def self.human_access_levels + { + Gitlab::Access::MASTER => "Masters", + Gitlab::Access::DEVELOPER => "Developers + Masters", + Gitlab::Access::NO_ACCESS => "No one" + }.with_indifferent_access + end + + def check_access(user) + return false if access_level == Gitlab::Access::NO_ACCESS + + super + end end end diff --git a/app/models/protected_branch/merge_access_level.rb b/app/models/protected_branch/merge_access_level.rb index 771e3376613..e8d35ac326f 100644 --- a/app/models/protected_branch/merge_access_level.rb +++ b/app/models/protected_branch/merge_access_level.rb @@ -1,13 +1,3 @@ class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base include ProtectedBranchAccess - - validates :access_level, presence: true, inclusion: { in: [Gitlab::Access::MASTER, - Gitlab::Access::DEVELOPER] } - - def self.human_access_levels - { - Gitlab::Access::MASTER => "Masters", - Gitlab::Access::DEVELOPER => "Developers + Masters" - }.with_indifferent_access - end end diff --git a/app/models/protected_branch/push_access_level.rb b/app/models/protected_branch/push_access_level.rb index 14610cb42b7..7a2e9e5ec5d 100644 --- a/app/models/protected_branch/push_access_level.rb +++ b/app/models/protected_branch/push_access_level.rb @@ -1,21 +1,3 @@ class ProtectedBranch::PushAccessLevel < ActiveRecord::Base include ProtectedBranchAccess - - validates :access_level, presence: true, inclusion: { in: [Gitlab::Access::MASTER, - Gitlab::Access::DEVELOPER, - Gitlab::Access::NO_ACCESS] } - - def self.human_access_levels - { - Gitlab::Access::MASTER => "Masters", - Gitlab::Access::DEVELOPER => "Developers + Masters", - Gitlab::Access::NO_ACCESS => "No one" - }.with_indifferent_access - end - - def check_access(user) - return false if access_level == Gitlab::Access::NO_ACCESS - - super - end end diff --git a/changelogs/unreleased/protected-branches-no-one-merge.yml b/changelogs/unreleased/protected-branches-no-one-merge.yml new file mode 100644 index 00000000000..52d93793f3d --- /dev/null +++ b/changelogs/unreleased/protected-branches-no-one-merge.yml @@ -0,0 +1,4 @@ +--- +title: Allow 'no one' as an option for allowed to merge on a procted branch +merge_request: +author: diff --git a/spec/features/protected_branches/access_control_ce_spec.rb b/spec/features/protected_branches/access_control_ce_spec.rb index d30e7947106..7fda4ade665 100644 --- a/spec/features/protected_branches/access_control_ce_spec.rb +++ b/spec/features/protected_branches/access_control_ce_spec.rb @@ -31,7 +31,7 @@ RSpec.shared_examples "protected branches > access control > CE" do within(".protected-branches-list") do find(".js-allowed-to-push").click - + within('.js-allowed-to-push-container') do expect(first("li")).to have_content("Roles") click_on access_type_name -- cgit v1.2.1 From 5a95d6f8dae00b31b694759c6ddbf6d83b1a7890 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 10 May 2017 12:29:33 +0100 Subject: Refactored issue tealtime elements This is to match our docs better and will also help a future issue. Also made it possible for the description & title to be readable when JS is disabled --- app/assets/javascripts/issue_show/actions/tasks.js | 27 ---- .../javascripts/issue_show/components/app.vue | 95 +++++++++++ .../issue_show/components/description.vue | 100 ++++++++++++ .../javascripts/issue_show/components/title.vue | 53 ++++++ app/assets/javascripts/issue_show/index.js | 34 ++-- .../issue_show/issue_title_description.vue | 180 --------------------- .../javascripts/issue_show/mixins/animate.js | 13 ++ .../javascripts/issue_show/services/index.js | 14 +- app/assets/javascripts/issue_show/stores/index.js | 25 +++ app/views/projects/issues/show.html.haml | 11 +- 10 files changed, 327 insertions(+), 225 deletions(-) delete mode 100644 app/assets/javascripts/issue_show/actions/tasks.js create mode 100644 app/assets/javascripts/issue_show/components/app.vue create mode 100644 app/assets/javascripts/issue_show/components/description.vue create mode 100644 app/assets/javascripts/issue_show/components/title.vue delete mode 100644 app/assets/javascripts/issue_show/issue_title_description.vue create mode 100644 app/assets/javascripts/issue_show/mixins/animate.js create mode 100644 app/assets/javascripts/issue_show/stores/index.js diff --git a/app/assets/javascripts/issue_show/actions/tasks.js b/app/assets/javascripts/issue_show/actions/tasks.js deleted file mode 100644 index 0740a9f559c..00000000000 --- a/app/assets/javascripts/issue_show/actions/tasks.js +++ /dev/null @@ -1,27 +0,0 @@ -export default (newStateData, tasks) => { - const $tasks = $('#task_status'); - const $tasksShort = $('#task_status_short'); - const $issueableHeader = $('.issuable-header'); - const tasksStates = { newState: null, currentState: null }; - - if ($tasks.length === 0) { - if (!(newStateData.task_status.indexOf('0 of 0') === 0)) { - $issueableHeader.append(`${newStateData.task_status}`); - } else { - $issueableHeader.append(''); - } - } else { - tasksStates.newState = newStateData.task_status.indexOf('0 of 0') === 0; - tasksStates.currentState = tasks.indexOf('0 of 0') === 0; - } - - if ($tasks.length !== 0 && !tasksStates.newState) { - $tasks.text(newStateData.task_status); - $tasksShort.text(newStateData.task_status); - } else if (tasksStates.currentState) { - $issueableHeader.append(`${newStateData.task_status}`); - } else if (tasksStates.newState) { - $tasks.remove(); - $tasksShort.remove(); - } -}; diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue new file mode 100644 index 00000000000..752d07f7ef0 --- /dev/null +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -0,0 +1,95 @@ + + + diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue new file mode 100644 index 00000000000..298f87b6d22 --- /dev/null +++ b/app/assets/javascripts/issue_show/components/description.vue @@ -0,0 +1,100 @@ + + + diff --git a/app/assets/javascripts/issue_show/components/title.vue b/app/assets/javascripts/issue_show/components/title.vue new file mode 100644 index 00000000000..a9dabd4cff1 --- /dev/null +++ b/app/assets/javascripts/issue_show/components/title.vue @@ -0,0 +1,53 @@ + + + diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js index eb20a597bb5..af11ae4c533 100644 --- a/app/assets/javascripts/issue_show/index.js +++ b/app/assets/javascripts/issue_show/index.js @@ -1,20 +1,32 @@ import Vue from 'vue'; -import IssueTitle from './issue_title_description.vue'; +import issuableApp from './components/app.vue'; import '../vue_shared/vue_resource_interceptor'; -(() => { - const issueTitleData = document.querySelector('.issue-title-data').dataset; - const { canUpdateTasksClass, endpoint } = issueTitleData; +document.addEventListener('DOMContentLoaded', () => { + const issuableElement = document.getElementById('js-issuable-app'); + const issuableTitleElement = issuableElement.querySelector('.title'); + const issuableDescriptionElement = issuableElement.querySelector('.wiki'); + const issuableDescriptionTextarea = issuableElement.querySelector('.js-task-list-field'); + const { + canUpdate, + endpoint, + issuableRef, + } = issuableElement.dataset; - const vm = new Vue({ - el: '.issue-title-entrypoint', - render: createElement => createElement(IssueTitle, { + return new Vue({ + el: issuableElement, + components: { + issuableApp, + }, + render: createElement => createElement('issuable-app', { props: { - canUpdateTasksClass, + canUpdate: gl.utils.convertPermissionToBoolean(canUpdate), endpoint, + issuableRef, + initialTitle: issuableTitleElement.innerHTML, + initialDescriptionHtml: issuableDescriptionElement ? issuableDescriptionElement.innerHTML : '', + initialDescriptionText: issuableDescriptionTextarea ? issuableDescriptionTextarea.textContent : '', }, }), }); - - return vm; -})(); +}); diff --git a/app/assets/javascripts/issue_show/issue_title_description.vue b/app/assets/javascripts/issue_show/issue_title_description.vue deleted file mode 100644 index dc3ba2550c5..00000000000 --- a/app/assets/javascripts/issue_show/issue_title_description.vue +++ /dev/null @@ -1,180 +0,0 @@ - - - diff --git a/app/assets/javascripts/issue_show/mixins/animate.js b/app/assets/javascripts/issue_show/mixins/animate.js new file mode 100644 index 00000000000..eda6302aa8b --- /dev/null +++ b/app/assets/javascripts/issue_show/mixins/animate.js @@ -0,0 +1,13 @@ +export default { + methods: { + animateChange() { + this.preAnimation = true; + this.pulseAnimation = false; + + this.$nextTick(() => { + this.preAnimation = false; + this.pulseAnimation = true; + }); + }, + }, +}; diff --git a/app/assets/javascripts/issue_show/services/index.js b/app/assets/javascripts/issue_show/services/index.js index c4ab0b1e07a..348ad8d6813 100644 --- a/app/assets/javascripts/issue_show/services/index.js +++ b/app/assets/javascripts/issue_show/services/index.js @@ -1,10 +1,16 @@ +import Vue from 'vue'; +import VueResource from 'vue-resource'; + +Vue.use(VueResource); + export default class Service { - constructor(resource, endpoint) { - this.resource = resource; + constructor(endpoint) { this.endpoint = endpoint; + + this.resource = Vue.resource(this.endpoint); } - getTitle() { - return this.resource.get(this.endpoint); + getData() { + return this.resource.get(); } } diff --git a/app/assets/javascripts/issue_show/stores/index.js b/app/assets/javascripts/issue_show/stores/index.js new file mode 100644 index 00000000000..9c759dc53cb --- /dev/null +++ b/app/assets/javascripts/issue_show/stores/index.js @@ -0,0 +1,25 @@ +export default class Store { + constructor({ + title, + descriptionHtml, + descriptionText, + }) { + this.state = { + titleHtml: title, + titleText: '', + descriptionHtml, + descriptionText, + taskStatus: '', + updatedAt: '', + }; + } + + updateState(data) { + this.state.titleHtml = data.title; + this.state.titleText = data.title_text; + this.state.descriptionHtml = data.description; + this.state.descriptionText = data.description_text; + this.state.taskStatus = data.task_status; + this.state.updatedAt = data.updated_at; + } +} diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 9084883eb3e..bd03593eb98 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -51,10 +51,15 @@ .issue-details.issuable-details .detail-page-description.content-block - .issue-title-data.hidden{ "data" => { "endpoint" => rendered_title_namespace_project_issue_path(@project.namespace, @project, @issue), - "can-update-tasks-class" => can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '', + #js-issuable-app{ "data" => { "endpoint" => rendered_title_namespace_project_issue_path(@project.namespace, @project, @issue), + "can-update" => can?(current_user, :update_issue, @issue).to_s, + "issuable-ref" => @issue.to_reference, } } - .issue-title-entrypoint + %h2.title= markdown_field(@issue, :title) + - if @issue.description.present? + .description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' } + .wiki= markdown_field(@issue, :description) + %textarea.hidden.js-task-list-field= @issue.description = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago') -- cgit v1.2.1 From 5a5b06de3eb9c9607c31d9e788fec33b2ee8078e Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 10 May 2017 12:59:37 +0100 Subject: Fixed task status with mobile --- .../javascripts/issue_show/components/description.vue | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue index 298f87b6d22..1f594e0d5ea 100644 --- a/app/assets/javascripts/issue_show/components/description.vue +++ b/app/assets/javascripts/issue_show/components/description.vue @@ -56,19 +56,25 @@ }); }, taskStatus() { - const $issueableHeader = $('.issuable-header'); + const taskRegexMatches = this.taskStatus.match(/(\d+) of (\d+)/); + const $issueableHeader = $('.issuable-meta'); let $tasks = $('#task_status'); let $tasksShort = $('#task_status_short'); - if (this.taskStatus.indexOf('0 of 0') >= 0) { + if (this.taskStatus.indexOf('0 of 0') >= 0 || this.taskStatus.trim() === '') { $tasks.remove(); $tasksShort.remove(); } else if (!$tasks.length && !$tasksShort.length) { - $tasks = $issueableHeader.append(''); - $tasksShort = $issueableHeader.append(''); + $tasks = $issueableHeader.append('') + .find('#task_status'); + $tasksShort = $issueableHeader.append('') + .find('#task_status_short'); } - $tasks.text(this.taskStatus); + if (taskRegexMatches) { + $tasks.text(this.taskStatus); + $tasksShort.text(`${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ? 's' : ''}`); + } }, }, }; -- cgit v1.2.1 From 3313df580c79435187ba673499b48ba3be783cea Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 10 May 2017 15:05:58 +0100 Subject: Added a bunch of specs for the different components --- .../issue_show/components/description.vue | 10 +-- spec/javascripts/issue_show/components/app_spec.js | 60 +++++++++++++ .../issue_show/components/description_spec.js | 98 ++++++++++++++++++++++ .../issue_show/components/title_spec.js | 67 +++++++++++++++ .../issue_show/issue_title_description_spec.js | 60 ------------- 5 files changed, 230 insertions(+), 65 deletions(-) create mode 100644 spec/javascripts/issue_show/components/app_spec.js create mode 100644 spec/javascripts/issue_show/components/description_spec.js create mode 100644 spec/javascripts/issue_show/components/title_spec.js delete mode 100644 spec/javascripts/issue_show/issue_title_description_spec.js diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue index 1f594e0d5ea..18e73960e34 100644 --- a/app/assets/javascripts/issue_show/components/description.vue +++ b/app/assets/javascripts/issue_show/components/description.vue @@ -57,17 +57,17 @@ }, taskStatus() { const taskRegexMatches = this.taskStatus.match(/(\d+) of (\d+)/); - const $issueableHeader = $('.issuable-meta'); - let $tasks = $('#task_status'); - let $tasksShort = $('#task_status_short'); + const $issuableHeader = $('.issuable-meta'); + let $tasks = $('#task_status', $issuableHeader); + let $tasksShort = $('#task_status_short', $issuableHeader); if (this.taskStatus.indexOf('0 of 0') >= 0 || this.taskStatus.trim() === '') { $tasks.remove(); $tasksShort.remove(); } else if (!$tasks.length && !$tasksShort.length) { - $tasks = $issueableHeader.append('') + $tasks = $issuableHeader.append('') .find('#task_status'); - $tasksShort = $issueableHeader.append('') + $tasksShort = $issuableHeader.append('') .find('#task_status_short'); } diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js new file mode 100644 index 00000000000..c33321814a5 --- /dev/null +++ b/spec/javascripts/issue_show/components/app_spec.js @@ -0,0 +1,60 @@ +import Vue from 'vue'; +import '~/render_math'; +import '~/render_gfm'; +import issuableApp from '~/issue_show/components/app.vue'; +import issueShowData from '../mock_data'; + +const issueShowInterceptor = data => (request, next) => { + next(request.respondWith(JSON.stringify(data), { + status: 200, + headers: { + 'POLL-INTERVAL': 1, + }, + })); +}; + +describe('Issuable output', () => { + document.body.innerHTML = ''; + + let vm; + + beforeEach(() => { + const IssuableDescriptionComponent = Vue.extend(issuableApp); + Vue.http.interceptors.push(issueShowInterceptor(issueShowData.initialRequest)); + + vm = new IssuableDescriptionComponent({ + propsData: { + canUpdate: true, + endpoint: '/gitlab-org/gitlab-shell/issues/9/rendered_title', + issuableRef: '#1', + initialTitle: '', + initialDescriptionHtml: '', + initialDescriptionText: '', + }, + }).$mount(); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, issueShowInterceptor); + }); + + it('should render a title/description and update title/description on update', (done) => { + setTimeout(() => { + expect(document.querySelector('title').innerText).toContain('this is a title (#1)'); + expect(vm.$el.querySelector('.title').innerHTML).toContain('

this is a title

'); + expect(vm.$el.querySelector('.wiki').innerHTML).toContain('

this is a description!

'); + expect(vm.$el.querySelector('.js-task-list-field').innerText).toContain('this is a description'); + + Vue.http.interceptors.push(issueShowInterceptor(issueShowData.secondRequest)); + + setTimeout(() => { + expect(document.querySelector('title').innerText).toContain('2 (#1)'); + expect(vm.$el.querySelector('.title').innerHTML).toContain('

2

'); + expect(vm.$el.querySelector('.wiki').innerHTML).toContain('

42

'); + expect(vm.$el.querySelector('.js-task-list-field').innerText).toContain('42'); + + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/issue_show/components/description_spec.js b/spec/javascripts/issue_show/components/description_spec.js new file mode 100644 index 00000000000..df9941a2bdb --- /dev/null +++ b/spec/javascripts/issue_show/components/description_spec.js @@ -0,0 +1,98 @@ +import Vue from 'vue'; +import descriptionComponent from '~/issue_show/components/description.vue'; + +describe('Description component', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(descriptionComponent); + + if (!document.querySelector('.issuable-meta')) { + const metaData = document.createElement('div'); + metaData.classList.add('issuable-meta'); + + document.body.appendChild(metaData); + } + + vm = new Component({ + propsData: { + canUpdate: true, + descriptionHtml: 'test', + descriptionText: 'test', + updatedAt: new Date().toString(), + taskStatus: '', + }, + }).$mount(); + }); + + it('animates description changes', (done) => { + vm.descriptionHtml = 'changed'; + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.wiki').classList.contains('issue-realtime-pre-pulse'), + ).toBeTruthy(); + + setTimeout(() => { + expect( + vm.$el.querySelector('.wiki').classList.contains('issue-realtime-trigger-pulse'), + ).toBeTruthy(); + + done(); + }); + }); + }); + + it('re-inits the TaskList when description changed', (done) => { + spyOn(gl, 'TaskList'); + vm.descriptionHtml = 'changed'; + + setTimeout(() => { + expect( + gl.TaskList, + ).toHaveBeenCalled(); + + done(); + }); + }); + + it('does not re-init the TaskList when canUpdate is false', (done) => { + spyOn(gl, 'TaskList'); + vm.canUpdate = false; + vm.descriptionHtml = 'changed'; + + setTimeout(() => { + expect( + gl.TaskList, + ).not.toHaveBeenCalled(); + + done(); + }); + }); + + describe('taskStatus', () => { + it('adds full taskStatus', (done) => { + vm.taskStatus = '1 of 1'; + + setTimeout(() => { + expect( + document.querySelector('.issuable-meta #task_status').textContent.trim(), + ).toBe('1 of 1'); + + done(); + }); + }); + + it('adds short taskStatus', (done) => { + vm.taskStatus = '1 of 1'; + + setTimeout(() => { + expect( + document.querySelector('.issuable-meta #task_status_short').textContent.trim(), + ).toBe('1/1 task'); + + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/issue_show/components/title_spec.js b/spec/javascripts/issue_show/components/title_spec.js new file mode 100644 index 00000000000..2f953e7e92e --- /dev/null +++ b/spec/javascripts/issue_show/components/title_spec.js @@ -0,0 +1,67 @@ +import Vue from 'vue'; +import titleComponent from '~/issue_show/components/title.vue'; + +describe('Title component', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(titleComponent); + vm = new Component({ + propsData: { + issuableRef: '#1', + titleHtml: 'Testing ', + titleText: 'Testing', + }, + }).$mount(); + }); + + it('renders title HTML', () => { + expect( + vm.$el.innerHTML.trim(), + ).toBe('Testing '); + }); + + it('updates page title when changing titleHtml', (done) => { + spyOn(vm, 'setPageTitle'); + vm.titleHtml = 'test'; + + Vue.nextTick(() => { + expect( + vm.setPageTitle, + ).toHaveBeenCalled(); + + done(); + }); + }); + + it('animates title changes', (done) => { + vm.titleHtml = 'test'; + + Vue.nextTick(() => { + expect( + vm.$el.classList.contains('issue-realtime-pre-pulse'), + ).toBeTruthy(); + + setTimeout(() => { + expect( + vm.$el.classList.contains('issue-realtime-trigger-pulse'), + ).toBeTruthy(); + + done(); + }); + }); + }); + + it('updates page title after changing title', (done) => { + vm.titleHtml = 'changed'; + vm.titleText = 'changed'; + + Vue.nextTick(() => { + expect( + document.querySelector('title').textContent.trim(), + ).toContain('changed'); + + done(); + }); + }); +}); diff --git a/spec/javascripts/issue_show/issue_title_description_spec.js b/spec/javascripts/issue_show/issue_title_description_spec.js deleted file mode 100644 index 1ec4fe58b08..00000000000 --- a/spec/javascripts/issue_show/issue_title_description_spec.js +++ /dev/null @@ -1,60 +0,0 @@ -import Vue from 'vue'; -import $ from 'jquery'; -import '~/render_math'; -import '~/render_gfm'; -import issueTitleDescription from '~/issue_show/issue_title_description.vue'; -import issueShowData from './mock_data'; - -window.$ = $; - -const issueShowInterceptor = data => (request, next) => { - next(request.respondWith(JSON.stringify(data), { - status: 200, - headers: { - 'POLL-INTERVAL': 1, - }, - })); -}; - -describe('Issue Title', () => { - document.body.innerHTML = ''; - - let IssueTitleDescriptionComponent; - - beforeEach(() => { - IssueTitleDescriptionComponent = Vue.extend(issueTitleDescription); - }); - - afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, issueShowInterceptor); - }); - - it('should render a title/description and update title/description on update', (done) => { - Vue.http.interceptors.push(issueShowInterceptor(issueShowData.initialRequest)); - - const issueShowComponent = new IssueTitleDescriptionComponent({ - propsData: { - canUpdateIssue: '.css-stuff', - endpoint: '/gitlab-org/gitlab-shell/issues/9/rendered_title', - }, - }).$mount(); - - setTimeout(() => { - expect(document.querySelector('title').innerText).toContain('this is a title (#1)'); - expect(issueShowComponent.$el.querySelector('.title').innerHTML).toContain('

this is a title

'); - expect(issueShowComponent.$el.querySelector('.wiki').innerHTML).toContain('

this is a description!

'); - expect(issueShowComponent.$el.querySelector('.js-task-list-field').innerText).toContain('this is a description'); - - Vue.http.interceptors.push(issueShowInterceptor(issueShowData.secondRequest)); - - setTimeout(() => { - expect(document.querySelector('title').innerText).toContain('2 (#1)'); - expect(issueShowComponent.$el.querySelector('.title').innerHTML).toContain('

2

'); - expect(issueShowComponent.$el.querySelector('.wiki').innerHTML).toContain('

42

'); - expect(issueShowComponent.$el.querySelector('.js-task-list-field').innerText).toContain('42'); - - done(); - }); - }); - }); -}); -- cgit v1.2.1 From 598b05dfff2fa16f9dc835c3f2cea88da741a9d7 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 10 May 2017 17:28:36 +0100 Subject: Fixed a bunch of errors with invalid prop Use v-model on textrea --- .../javascripts/issue_show/components/app.vue | 3 +- .../issue_show/components/description.vue | 38 +++++++++++++--------- app/assets/javascripts/issue_show/stores/index.js | 4 +-- 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index 752d07f7ef0..770a0dcd27e 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -37,7 +37,7 @@ export default { }, data() { const store = new Store({ - title: this.initialTitle, + titleHtml: this.initialTitle, descriptionHtml: this.initialDescriptionHtml, descriptionText: this.initialDescriptionText, }); @@ -86,6 +86,7 @@ export default { :title-html="state.titleHtml" :title-text="state.titleText" /> diff --git a/app/assets/javascripts/issue_show/stores/index.js b/app/assets/javascripts/issue_show/stores/index.js index 9c759dc53cb..8e89a2b7730 100644 --- a/app/assets/javascripts/issue_show/stores/index.js +++ b/app/assets/javascripts/issue_show/stores/index.js @@ -1,11 +1,11 @@ export default class Store { constructor({ - title, + titleHtml, descriptionHtml, descriptionText, }) { this.state = { - titleHtml: title, + titleHtml, titleText: '', descriptionHtml, descriptionText, -- cgit v1.2.1 From 584ea586ff04bd57852748f4e6b046200eac7f68 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 10 May 2017 18:50:14 +0100 Subject: Fixed tests on textarea looking for innerText instead of value --- app/controllers/projects/issues_controller.rb | 1 - spec/javascripts/issue_show/components/app_spec.js | 6 +++--- spec/javascripts/issue_show/mock_data.js | 6 +++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 58d41e0478d..c9cd42cf99d 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -208,7 +208,6 @@ class Projects::IssuesController < Projects::ApplicationController description: view_context.markdown_field(@issue, :description), description_text: @issue.description, task_status: @issue.task_status, - issue_number: @issue.iid, updated_at: @issue.updated_at, } end diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js index c33321814a5..2c294eb4789 100644 --- a/spec/javascripts/issue_show/components/app_spec.js +++ b/spec/javascripts/issue_show/components/app_spec.js @@ -13,7 +13,7 @@ const issueShowInterceptor = data => (request, next) => { })); }; -describe('Issuable output', () => { +fdescribe('Issuable output', () => { document.body.innerHTML = ''; let vm; @@ -43,7 +43,7 @@ describe('Issuable output', () => { expect(document.querySelector('title').innerText).toContain('this is a title (#1)'); expect(vm.$el.querySelector('.title').innerHTML).toContain('

this is a title

'); expect(vm.$el.querySelector('.wiki').innerHTML).toContain('

this is a description!

'); - expect(vm.$el.querySelector('.js-task-list-field').innerText).toContain('this is a description'); + expect(vm.$el.querySelector('.js-task-list-field').value).toContain('this is a description'); Vue.http.interceptors.push(issueShowInterceptor(issueShowData.secondRequest)); @@ -51,7 +51,7 @@ describe('Issuable output', () => { expect(document.querySelector('title').innerText).toContain('2 (#1)'); expect(vm.$el.querySelector('.title').innerHTML).toContain('

2

'); expect(vm.$el.querySelector('.wiki').innerHTML).toContain('

42

'); - expect(vm.$el.querySelector('.js-task-list-field').innerText).toContain('42'); + expect(vm.$el.querySelector('.js-task-list-field').value).toContain('42'); done(); }); diff --git a/spec/javascripts/issue_show/mock_data.js b/spec/javascripts/issue_show/mock_data.js index ad5a7b63470..6683d581bc5 100644 --- a/spec/javascripts/issue_show/mock_data.js +++ b/spec/javascripts/issue_show/mock_data.js @@ -4,23 +4,23 @@ export default { title_text: 'this is a title', description: '

this is a description!

', description_text: 'this is a description', - issue_number: 1, task_status: '2 of 4 completed', + updated_at: new Date().toString(), }, secondRequest: { title: '

2

', title_text: '2', description: '

42

', description_text: '42', - issue_number: 1, task_status: '0 of 0 completed', + updated_at: new Date().toString(), }, issueSpecRequest: { title: '

this is a title

', title_text: 'this is a title', description: '
  • Task List Item
  • ', description_text: '- [ ] Task List Item', - issue_number: 1, task_status: '0 of 1 completed', + updated_at: new Date().toString(), }, }; -- cgit v1.2.1 From 4a4b586aa0cd1298d515fb172a7bd90ff3591970 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Wed, 10 May 2017 20:41:06 +0100 Subject: Removed vue and vue-resource from poll_spec in an attempt to fix the transient failures relating to async timeout --- spec/javascripts/lib/utils/poll_spec.js | 87 +++++++++++---------------------- 1 file changed, 29 insertions(+), 58 deletions(-) diff --git a/spec/javascripts/lib/utils/poll_spec.js b/spec/javascripts/lib/utils/poll_spec.js index 918b6d32c43..bba7fc387ff 100644 --- a/spec/javascripts/lib/utils/poll_spec.js +++ b/spec/javascripts/lib/utils/poll_spec.js @@ -1,9 +1,5 @@ -import Vue from 'vue'; -import VueResource from 'vue-resource'; import Poll from '~/lib/utils/poll'; -Vue.use(VueResource); - const waitForAllCallsToFinish = (service, waitForCount, successCallback) => { const timer = () => { setTimeout(() => { @@ -12,25 +8,20 @@ const waitForAllCallsToFinish = (service, waitForCount, successCallback) => { } else { timer(); } - }, 5); + }, 0); }; timer(); }; -class ServiceMock { - constructor(endpoint) { - this.service = Vue.resource(endpoint); - } - - fetch() { - return this.service.get(); - } +function mockServiceCall(service, mockCall) { + service.fetch.calls.reset(); + service.fetch.and.callFake(mockCall); } describe('Poll', () => { + const service = jasmine.createSpyObj('service', ['fetch']); let callbacks; - let service; beforeEach(() => { callbacks = { @@ -38,19 +29,15 @@ describe('Poll', () => { error: () => {}, }; - service = new ServiceMock('endpoint'); - spyOn(callbacks, 'success'); spyOn(callbacks, 'error'); - spyOn(service, 'fetch').and.callThrough(); }); it('calls the success callback when no header for interval is provided', (done) => { - const successInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify([]), { status: 200 })); - }; - - Vue.http.interceptors.push(successInterceptor); + mockServiceCall( + service, + () => Promise.resolve({ status: 200, headers: {} }), + ); new Poll({ resource: service, @@ -63,18 +50,15 @@ describe('Poll', () => { expect(callbacks.success).toHaveBeenCalled(); expect(callbacks.error).not.toHaveBeenCalled(); - Vue.http.interceptors = _.without(Vue.http.interceptors, successInterceptor); - done(); }, 0); }); it('calls the error callback whe the http request returns an error', (done) => { - const errorInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify([]), { status: 500 })); - }; - - Vue.http.interceptors.push(errorInterceptor); + mockServiceCall( + service, + () => Promise.reject({ status: 500, headers: {} }), + ); new Poll({ resource: service, @@ -86,18 +70,16 @@ describe('Poll', () => { waitForAllCallsToFinish(service, 1, () => { expect(callbacks.success).not.toHaveBeenCalled(); expect(callbacks.error).toHaveBeenCalled(); - Vue.http.interceptors = _.without(Vue.http.interceptors, errorInterceptor); done(); }); }); it('should call the success callback when the interval header is -1', (done) => { - const intervalInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify([]), { status: 200, headers: { 'poll-interval': -1 } })); - }; - - Vue.http.interceptors.push(intervalInterceptor); + mockServiceCall( + service, + () => Promise.resolve({ status: 200, headers: { 'poll-interval': -1 } }), + ); new Poll({ resource: service, @@ -110,18 +92,15 @@ describe('Poll', () => { expect(callbacks.success).toHaveBeenCalled(); expect(callbacks.error).not.toHaveBeenCalled(); - Vue.http.interceptors = _.without(Vue.http.interceptors, intervalInterceptor); - done(); }, 0); }); it('starts polling when http status is 200 and interval header is provided', (done) => { - const pollInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify([]), { status: 200, headers: { 'poll-interval': 2 } })); - }; - - Vue.http.interceptors.push(pollInterceptor); + mockServiceCall( + service, + () => Promise.resolve({ status: 200, headers: { 'poll-interval': 1 } }), + ); const Polling = new Poll({ resource: service, @@ -141,19 +120,16 @@ describe('Poll', () => { expect(callbacks.success).toHaveBeenCalled(); expect(callbacks.error).not.toHaveBeenCalled(); - Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor); - done(); }); }); describe('stop', () => { it('stops polling when method is called', (done) => { - const pollInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify([]), { status: 200, headers: { 'poll-interval': 2 } })); - }; - - Vue.http.interceptors.push(pollInterceptor); + mockServiceCall( + service, + () => Promise.resolve({ status: 200, headers: { 'poll-interval': 1 } }), + ); const Polling = new Poll({ resource: service, @@ -174,8 +150,6 @@ describe('Poll', () => { expect(service.fetch).toHaveBeenCalledWith({ page: 1 }); expect(Polling.stop).toHaveBeenCalled(); - Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor); - done(); }); }); @@ -183,11 +157,10 @@ describe('Poll', () => { describe('restart', () => { it('should restart polling when its called', (done) => { - const pollInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify([]), { status: 200, headers: { 'poll-interval': 2 } })); - }; - - Vue.http.interceptors.push(pollInterceptor); + mockServiceCall( + service, + () => Promise.resolve({ status: 200, headers: { 'poll-interval': 1 } }), + ); const Polling = new Poll({ resource: service, @@ -215,8 +188,6 @@ describe('Poll', () => { expect(Polling.stop).toHaveBeenCalled(); expect(Polling.restart).toHaveBeenCalled(); - Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor); - done(); }); }); -- cgit v1.2.1 From 087ea0035fad513606ddb91d5bbb7f03236e614c Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Wed, 10 May 2017 21:24:21 -0500 Subject: Fix new branch dropdown position and size Fix https://gitlab.com/gitlab-org/gitlab-ce/issues/32096 --- app/views/projects/branches/new.html.haml | 12 +++++++----- .../projects/branches/new_branch_ref_dropdown_spec.rb | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml index 796ecdfd014..55575c5e412 100644 --- a/app/views/projects/branches/new.html.haml +++ b/app/views/projects/branches/new.html.haml @@ -17,11 +17,13 @@ .help-block.text-danger.js-branch-name-error .form-group = label_tag :ref, 'Create from', class: 'control-label' - .col-sm-10.dropdown.create-from - = hidden_field_tag :ref, default_ref - = button_tag type: 'button', title: default_ref, class: 'dropdown-toggle form-control js-branch-select', required: true, data: { toggle: 'dropdown', selected: default_ref, field_name: 'ref' } do - .text-left.dropdown-toggle-text= default_ref - = render 'shared/ref_dropdown', dropdown_class: 'wide' + .col-sm-10.create-from + .dropdown + = hidden_field_tag :ref, default_ref + = button_tag type: 'button', title: default_ref, class: 'dropdown-menu-toggle wide form-control js-branch-select', required: true, data: { toggle: 'dropdown', selected: default_ref, field_name: 'ref' } do + .text-left.dropdown-toggle-text= default_ref + = icon('chevron-down') + = render 'shared/ref_dropdown', dropdown_class: 'wide' .help-block Existing branch name, tag, or commit SHA .form-actions = button_tag 'Create branch', class: 'btn btn-create', tabindex: 3 diff --git a/spec/features/projects/branches/new_branch_ref_dropdown_spec.rb b/spec/features/projects/branches/new_branch_ref_dropdown_spec.rb index cfc782c98ad..c5e0a0f0517 100644 --- a/spec/features/projects/branches/new_branch_ref_dropdown_spec.rb +++ b/spec/features/projects/branches/new_branch_ref_dropdown_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe 'New Branch Ref Dropdown', :js, :feature do let(:user) { create(:user) } let(:project) { create(:project, :public) } - let(:toggle) { find('.create-from .dropdown-toggle') } + let(:toggle) { find('.create-from .dropdown-menu-toggle') } before do project.add_master(user) -- cgit v1.2.1 From 0c55c8891b891eadef41026420f955bb504f305f Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 11 May 2017 12:25:22 +0100 Subject: Remove some weird code to add/remove the task status Moved the data into the data method Renamed edited ago class name --- .../issue_show/components/description.vue | 19 +++---- app/assets/javascripts/issue_show/index.js | 60 +++++++++++++--------- app/assets/stylesheets/framework/mobile.scss | 2 +- app/helpers/issuables_helper.rb | 8 ++- app/views/projects/issues/show.html.haml | 2 +- 5 files changed, 46 insertions(+), 45 deletions(-) diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue index 770bf68e093..4ad3eb7dfd7 100644 --- a/app/assets/javascripts/issue_show/components/description.vue +++ b/app/assets/javascripts/issue_show/components/description.vue @@ -29,7 +29,7 @@ return { preAnimation: false, pulseAnimation: false, - timeAgoEl: $('.issue_edited_ago'), + timeAgoEl: $('.js-issue-edited-ago'), }; }, watch: { @@ -49,22 +49,15 @@ taskStatus() { const taskRegexMatches = this.taskStatus.match(/(\d+) of (\d+)/); const $issuableHeader = $('.issuable-meta'); - let $tasks = $('#task_status', $issuableHeader); - let $tasksShort = $('#task_status_short', $issuableHeader); - - if (this.taskStatus.indexOf('0 of 0') >= 0 || this.taskStatus.trim() === '') { - $tasks.remove(); - $tasksShort.remove(); - } else if (!$tasks.length && !$tasksShort.length) { - $tasks = $issuableHeader.append('') - .find('#task_status'); - $tasksShort = $issuableHeader.append('') - .find('#task_status_short'); - } + const $tasks = $('#task_status', $issuableHeader); + const $tasksShort = $('#task_status_short', $issuableHeader); if (taskRegexMatches) { $tasks.text(this.taskStatus); $tasksShort.text(`${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ? 's' : ''}`); + } else { + $tasks.text(''); + $tasksShort.text(''); } }, }, diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js index af11ae4c533..f06e33dee60 100644 --- a/app/assets/javascripts/issue_show/index.js +++ b/app/assets/javascripts/issue_show/index.js @@ -2,31 +2,41 @@ import Vue from 'vue'; import issuableApp from './components/app.vue'; import '../vue_shared/vue_resource_interceptor'; -document.addEventListener('DOMContentLoaded', () => { - const issuableElement = document.getElementById('js-issuable-app'); - const issuableTitleElement = issuableElement.querySelector('.title'); - const issuableDescriptionElement = issuableElement.querySelector('.wiki'); - const issuableDescriptionTextarea = issuableElement.querySelector('.js-task-list-field'); - const { - canUpdate, - endpoint, - issuableRef, - } = issuableElement.dataset; +document.addEventListener('DOMContentLoaded', () => new Vue({ + el: document.getElementById('js-issuable-app'), + components: { + issuableApp, + }, + data() { + const issuableElement = this.$options.el; + const issuableTitleElement = issuableElement.querySelector('.title'); + const issuableDescriptionElement = issuableElement.querySelector('.wiki'); + const issuableDescriptionTextarea = issuableElement.querySelector('.js-task-list-field'); + const { + canUpdate, + endpoint, + issuableRef, + } = issuableElement.dataset; - return new Vue({ - el: issuableElement, - components: { - issuableApp, - }, - render: createElement => createElement('issuable-app', { + return { + canUpdate: gl.utils.convertPermissionToBoolean(canUpdate), + endpoint, + issuableRef, + initialTitle: issuableTitleElement.innerHTML, + initialDescriptionHtml: issuableDescriptionElement ? issuableDescriptionElement.innerHTML : '', + initialDescriptionText: issuableDescriptionTextarea ? issuableDescriptionTextarea.textContent : '', + }; + }, + render(createElement) { + return createElement('issuable-app', { props: { - canUpdate: gl.utils.convertPermissionToBoolean(canUpdate), - endpoint, - issuableRef, - initialTitle: issuableTitleElement.innerHTML, - initialDescriptionHtml: issuableDescriptionElement ? issuableDescriptionElement.innerHTML : '', - initialDescriptionText: issuableDescriptionTextarea ? issuableDescriptionTextarea.textContent : '', + canUpdate: this.canUpdate, + endpoint: this.endpoint, + issuableRef: this.issuableRef, + initialTitle: this.initialTitle, + initialDescriptionHtml: this.initialDescriptionHtml, + initialDescriptionText: this.initialDescriptionText, }, - }), - }); -}); + }); + }, +})); diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index eb73f7cc794..678af978edd 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -112,7 +112,7 @@ } } - .issue_edited_ago, + .issue-edited-ago, .note_edited_ago { display: none; } diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index fbbce6876c2..bc7ff99d483 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -136,11 +136,9 @@ module IssuablesHelper author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "hidden-sm hidden-md hidden-lg") end - if issuable.tasks? - output << " ".html_safe - output << content_tag(:span, issuable.task_status, id: "task_status", class: "hidden-xs hidden-sm") - output << content_tag(:span, issuable.task_status_short, id: "task_status_short", class: "hidden-md hidden-lg") - end + output << " ".html_safe + output << content_tag(:span, issuable.task_status, id: "task_status", class: "hidden-xs hidden-sm") + output << content_tag(:span, issuable.task_status_short, id: "task_status_short", class: "hidden-md hidden-lg") output end diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index bd03593eb98..f66724900de 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -61,7 +61,7 @@ .wiki= markdown_field(@issue, :description) %textarea.hidden.js-task-list-field= @issue.description - = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago') + = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue-edited-ago js-issue-edited-ago') #merge-requests{ data: { url: referenced_merge_requests_namespace_project_issue_url(@project.namespace, @project, @issue) } } // This element is filled in using JavaScript. -- cgit v1.2.1 From ba6f263dd28a9cec6a4834106bc420b4a8e45209 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 11 May 2017 12:33:05 +0100 Subject: Improve ci action icon function --- .../javascripts/vue_shared/ci_action_icons.js | 31 +++++++++------------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/app/assets/javascripts/vue_shared/ci_action_icons.js b/app/assets/javascripts/vue_shared/ci_action_icons.js index ee41dc95beb..b21f0ab49fd 100644 --- a/app/assets/javascripts/vue_shared/ci_action_icons.js +++ b/app/assets/javascripts/vue_shared/ci_action_icons.js @@ -3,24 +3,19 @@ import retrySVG from 'icons/_icon_action_retry.svg'; import playSVG from 'icons/_icon_action_play.svg'; import stopSVG from 'icons/_icon_action_stop.svg'; +/** + * For the provided action returns the respective SVG + * + * @param {String} action + * @return {SVG|String} + */ export default function getActionIcon(action) { - let icon; - switch (action) { - case 'icon_action_cancel': - icon = cancelSVG; - break; - case 'icon_action_retry': - icon = retrySVG; - break; - case 'icon_action_play': - icon = playSVG; - break; - case 'icon_action_stop': - icon = stopSVG; - break; - default: - icon = ''; - } + const icons = { + icon_action_cancel: cancelSVG, + icon_action_play: playSVG, + icon_action_retry: retrySVG, + icon_action_stop: stopSVG, + }; - return icon; + return icons[action] || ''; } -- cgit v1.2.1 From dbab076b05e4bbd6faa9f59cf07401de76081fd5 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Thu, 11 May 2017 12:48:24 +0100 Subject: improved mockservicecall and fixed up a settimeout --- spec/javascripts/lib/utils/poll_spec.js | 49 ++++++++++++--------------------- 1 file changed, 17 insertions(+), 32 deletions(-) diff --git a/spec/javascripts/lib/utils/poll_spec.js b/spec/javascripts/lib/utils/poll_spec.js index bba7fc387ff..b9e1cc005fb 100644 --- a/spec/javascripts/lib/utils/poll_spec.js +++ b/spec/javascripts/lib/utils/poll_spec.js @@ -14,12 +14,17 @@ const waitForAllCallsToFinish = (service, waitForCount, successCallback) => { timer(); }; -function mockServiceCall(service, mockCall) { +function mockServiceCall(service, response, shouldFail) { + const action = shouldFail ? Promise.reject : Promise.resolve; + const responseObject = response; + + if (!responseObject.headers) responseObject.headers = {}; + service.fetch.calls.reset(); - service.fetch.and.callFake(mockCall); + service.fetch.and.callFake(() => action(responseObject)); } -describe('Poll', () => { +fdescribe('Poll', () => { const service = jasmine.createSpyObj('service', ['fetch']); let callbacks; @@ -33,11 +38,8 @@ describe('Poll', () => { spyOn(callbacks, 'error'); }); - it('calls the success callback when no header for interval is provided', (done) => { - mockServiceCall( - service, - () => Promise.resolve({ status: 200, headers: {} }), - ); + fit('calls the success callback when no header for interval is provided', (done) => { + mockServiceCall(service, { status: 200 }); new Poll({ resource: service, @@ -55,10 +57,7 @@ describe('Poll', () => { }); it('calls the error callback whe the http request returns an error', (done) => { - mockServiceCall( - service, - () => Promise.reject({ status: 500, headers: {} }), - ); + mockServiceCall(service, { status: 500 }, true); new Poll({ resource: service, @@ -76,31 +75,23 @@ describe('Poll', () => { }); it('should call the success callback when the interval header is -1', (done) => { - mockServiceCall( - service, - () => Promise.resolve({ status: 200, headers: { 'poll-interval': -1 } }), - ); + mockServiceCall(service, { status: 200, headers: { 'poll-interval': -1 } }); new Poll({ resource: service, method: 'fetch', successCallback: callbacks.success, errorCallback: callbacks.error, - }).makeRequest(); - - setTimeout(() => { + }).makeRequest().then(() => { expect(callbacks.success).toHaveBeenCalled(); expect(callbacks.error).not.toHaveBeenCalled(); done(); - }, 0); + }).catch(done.fail); }); it('starts polling when http status is 200 and interval header is provided', (done) => { - mockServiceCall( - service, - () => Promise.resolve({ status: 200, headers: { 'poll-interval': 1 } }), - ); + mockServiceCall(service, { status: 200, headers: { 'poll-interval': 1 } }); const Polling = new Poll({ resource: service, @@ -126,10 +117,7 @@ describe('Poll', () => { describe('stop', () => { it('stops polling when method is called', (done) => { - mockServiceCall( - service, - () => Promise.resolve({ status: 200, headers: { 'poll-interval': 1 } }), - ); + mockServiceCall(service, { status: 200, headers: { 'poll-interval': 1 } }); const Polling = new Poll({ resource: service, @@ -157,10 +145,7 @@ describe('Poll', () => { describe('restart', () => { it('should restart polling when its called', (done) => { - mockServiceCall( - service, - () => Promise.resolve({ status: 200, headers: { 'poll-interval': 1 } }), - ); + mockServiceCall(service, { status: 200, headers: { 'poll-interval': 1 } }); const Polling = new Poll({ resource: service, -- cgit v1.2.1 From 2d7b9455e1067994d2f071ca790a731df16db9c3 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Thu, 11 May 2017 13:09:13 +0100 Subject: Fixed promise action binding and simplified callbacks spy --- spec/javascripts/lib/utils/poll_spec.js | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/spec/javascripts/lib/utils/poll_spec.js b/spec/javascripts/lib/utils/poll_spec.js index b9e1cc005fb..65e89fb10b4 100644 --- a/spec/javascripts/lib/utils/poll_spec.js +++ b/spec/javascripts/lib/utils/poll_spec.js @@ -14,32 +14,27 @@ const waitForAllCallsToFinish = (service, waitForCount, successCallback) => { timer(); }; -function mockServiceCall(service, response, shouldFail) { +function mockServiceCall(service, response, shouldFail = false) { const action = shouldFail ? Promise.reject : Promise.resolve; const responseObject = response; if (!responseObject.headers) responseObject.headers = {}; - service.fetch.calls.reset(); - service.fetch.and.callFake(() => action(responseObject)); + service.fetch.and.callFake(action.bind(Promise, responseObject)); } -fdescribe('Poll', () => { +describe('Poll', () => { const service = jasmine.createSpyObj('service', ['fetch']); - let callbacks; + const callbacks = jasmine.createSpyObj('callbacks', ['success', 'error']); - beforeEach(() => { - callbacks = { - success: () => {}, - error: () => {}, - }; - - spyOn(callbacks, 'success'); - spyOn(callbacks, 'error'); + afterEach(() => { + callbacks.success.calls.reset(); + callbacks.error.calls.reset(); + service.fetch.calls.reset(); }); - fit('calls the success callback when no header for interval is provided', (done) => { - mockServiceCall(service, { status: 200 }); + it('calls the success callback when no header for interval is provided', (done) => { + mockServiceCall(service, { status: 200 }, false); new Poll({ resource: service, @@ -53,7 +48,7 @@ fdescribe('Poll', () => { expect(callbacks.error).not.toHaveBeenCalled(); done(); - }, 0); + }); }); it('calls the error callback whe the http request returns an error', (done) => { -- cgit v1.2.1 From 3dfce3ab6b092ec40dc95fa292b87f841abf0eba Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 11 May 2017 14:34:56 +0100 Subject: Fixed karma spec with elements not appearing in DOM --- spec/javascripts/issue_show/components/description_spec.js | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/javascripts/issue_show/components/description_spec.js b/spec/javascripts/issue_show/components/description_spec.js index df9941a2bdb..408349cc42d 100644 --- a/spec/javascripts/issue_show/components/description_spec.js +++ b/spec/javascripts/issue_show/components/description_spec.js @@ -10,6 +10,7 @@ describe('Description component', () => { if (!document.querySelector('.issuable-meta')) { const metaData = document.createElement('div'); metaData.classList.add('issuable-meta'); + metaData.innerHTML = ''; document.body.appendChild(metaData); } -- cgit v1.2.1 From 6bcf316ed701620fa5ba654f2184a261650fd7b9 Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Tue, 9 May 2017 11:35:22 -0400 Subject: Pass docsUrl to pipeline schedules callout component. --- .../components/pipeline_schedules_callout.js | 12 ++++++++---- .../pipeline_schedules/pipeline_schedules_index_bundle.js | 15 +++++++++------ app/views/projects/pipeline_schedules/index.html.haml | 2 +- .../unreleased/pipeline-schedules-callout-docs-url.yml | 4 ++++ .../pipeline_schedules/pipeline_schedule_callout_spec.js | 15 +++++++++++++++ 5 files changed, 37 insertions(+), 11 deletions(-) create mode 100644 changelogs/unreleased/pipeline-schedules-callout-docs-url.yml diff --git a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.js b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.js index 27ffe6ea304..5109b110b31 100644 --- a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.js +++ b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.js @@ -4,8 +4,10 @@ import illustrationSvg from '../icons/intro_illustration.svg'; const cookieKey = 'pipeline_schedules_callout_dismissed'; export default { + name: 'PipelineSchedulesCallout', data() { return { + docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl, illustrationSvg, calloutDismissed: Cookies.get(cookieKey) === 'true', }; @@ -28,13 +30,15 @@ export default {

    Scheduling Pipelines

    -

    - The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. +

    + The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user.

    Learn more in the - - pipeline schedules documentation. + pipeline schedules documentation.

    diff --git a/app/assets/javascripts/pipeline_schedules/pipeline_schedules_index_bundle.js b/app/assets/javascripts/pipeline_schedules/pipeline_schedules_index_bundle.js index e36dc5db2ab..6584549ad06 100644 --- a/app/assets/javascripts/pipeline_schedules/pipeline_schedules_index_bundle.js +++ b/app/assets/javascripts/pipeline_schedules/pipeline_schedules_index_bundle.js @@ -1,9 +1,12 @@ import Vue from 'vue'; import PipelineSchedulesCallout from './components/pipeline_schedules_callout'; -const PipelineSchedulesCalloutComponent = Vue.extend(PipelineSchedulesCallout); - -document.addEventListener('DOMContentLoaded', () => { - new PipelineSchedulesCalloutComponent() - .$mount('#scheduling-pipelines-callout'); -}); +document.addEventListener('DOMContentLoaded', () => new Vue({ + el: '#pipeline-schedules-callout', + components: { + 'pipeline-schedules-callout': PipelineSchedulesCallout, + }, + render(createElement) { + return createElement('pipeline-schedules-callout'); + }, +})); diff --git a/app/views/projects/pipeline_schedules/index.html.haml b/app/views/projects/pipeline_schedules/index.html.haml index dd35c3055f2..a597d745e33 100644 --- a/app/views/projects/pipeline_schedules/index.html.haml +++ b/app/views/projects/pipeline_schedules/index.html.haml @@ -6,7 +6,7 @@ = render "projects/pipelines/head" %div{ class: container_class } - #scheduling-pipelines-callout + #pipeline-schedules-callout{ data: { docs_url: help_page_path('ci/pipeline_schedules') } } .top-area - schedule_path_proc = ->(scope) { pipeline_schedules_path(@project, scope: scope) } = render "tabs", schedule_path_proc: schedule_path_proc, all_schedules: @all_schedules, scope: @scope diff --git a/changelogs/unreleased/pipeline-schedules-callout-docs-url.yml b/changelogs/unreleased/pipeline-schedules-callout-docs-url.yml new file mode 100644 index 00000000000..b21bb162380 --- /dev/null +++ b/changelogs/unreleased/pipeline-schedules-callout-docs-url.yml @@ -0,0 +1,4 @@ +--- +title: Pass docsUrl to pipeline schedules callout component. +merge_request: !1126 +author: diff --git a/spec/javascripts/pipeline_schedules/pipeline_schedule_callout_spec.js b/spec/javascripts/pipeline_schedules/pipeline_schedule_callout_spec.js index 1d05f37cb36..6120d224ac0 100644 --- a/spec/javascripts/pipeline_schedules/pipeline_schedule_callout_spec.js +++ b/spec/javascripts/pipeline_schedules/pipeline_schedule_callout_spec.js @@ -4,8 +4,15 @@ import PipelineSchedulesCallout from '~/pipeline_schedules/components/pipeline_s const PipelineSchedulesCalloutComponent = Vue.extend(PipelineSchedulesCallout); const cookieKey = 'pipeline_schedules_callout_dismissed'; +const docsUrl = 'help/ci/scheduled_pipelines'; describe('Pipeline Schedule Callout', () => { + beforeEach(() => { + setFixtures(` +
    + `); + }); + describe('independent of cookies', () => { beforeEach(() => { this.calloutComponent = new PipelineSchedulesCalloutComponent().$mount(); @@ -18,6 +25,10 @@ describe('Pipeline Schedule Callout', () => { it('correctly sets illustrationSvg', () => { expect(this.calloutComponent.illustrationSvg).toContain(' { + expect(this.calloutComponent.docsUrl).toContain(docsUrl); + }); }); describe(`when ${cookieKey} cookie is set`, () => { @@ -61,6 +72,10 @@ describe('Pipeline Schedule Callout', () => { expect(this.calloutComponent.$el.outerHTML).toContain('runs pipelines in the future'); }); + it('renders the documentation url', () => { + expect(this.calloutComponent.$el.outerHTML).toContain(docsUrl); + }); + it('updates calloutDismissed when close button is clicked', (done) => { this.calloutComponent.$el.querySelector('#dismiss-callout-btn').click(); -- cgit v1.2.1 From 27fb64d6a33ea61ba5a0a3ad50babff8c30fa45f Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 11 May 2017 10:52:45 +0200 Subject: Remove deltas_only from DiffCollection --- app/models/commit.rb | 5 +---- lib/gitlab/git/diff_collection.rb | 11 ----------- spec/models/commit_spec.rb | 21 ++++----------------- 3 files changed, 5 insertions(+), 32 deletions(-) diff --git a/app/models/commit.rb b/app/models/commit.rb index 3a143a5a1f6..8d54ce6eb25 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -326,10 +326,7 @@ class Commit end def raw_diffs(*args) - use_gitaly = Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs) - deltas_only = args.last.is_a?(Hash) && args.last[:deltas_only] - - if use_gitaly && !deltas_only + if Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs) Gitlab::GitalyClient::Commit.diff_from_parent(self, *args) else raw.diffs(*args) diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb index 4e45ec7c174..bcbad8ec829 100644 --- a/lib/gitlab/git/diff_collection.rb +++ b/lib/gitlab/git/diff_collection.rb @@ -15,7 +15,6 @@ module Gitlab @safe_max_bytes = @safe_max_files * 5120 # Average 5 KB per file @all_diffs = !!options.fetch(:all_diffs, false) @no_collapse = !!options.fetch(:no_collapse, true) - @deltas_only = !!options.fetch(:deltas_only, false) @line_count = 0 @byte_count = 0 @@ -27,8 +26,6 @@ module Gitlab if @populated # @iterator.each is slower than just iterating the array in place @array.each(&block) - elsif @deltas_only - each_delta(&block) else Gitlab::GitalyClient.migrate(:commit_raw_diffs) do each_patch(&block) @@ -81,14 +78,6 @@ module Gitlab files >= @safe_max_files || @line_count > @safe_max_lines || @byte_count >= @safe_max_bytes end - def each_delta - @iterator.each_delta.with_index do |delta, i| - diff = Gitlab::Git::Diff.new(delta) - - yield @array[i] = diff - end - end - def each_patch @iterator.each_with_index do |raw, i| # First yield cached Diff instances from @array diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 852889d4540..131fea8be43 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -395,24 +395,11 @@ eos allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:commit_raw_diffs).and_return(true) end - context 'when a truthy deltas_only is not passed to args' do - it 'fetches diffs from Gitaly server' do - expect(Gitlab::GitalyClient::Commit).to receive(:diff_from_parent). - with(commit) + it 'fetches diffs from Gitaly server' do + expect(Gitlab::GitalyClient::Commit).to receive(:diff_from_parent). + with(commit) - commit.raw_diffs - end - end - - context 'when a truthy deltas_only is passed to args' do - it 'fetches diffs using Rugged' do - opts = { deltas_only: true } - - expect(Gitlab::GitalyClient::Commit).not_to receive(:diff_from_parent) - expect(commit.raw).to receive(:diffs).with(opts) - - commit.raw_diffs(opts) - end + commit.raw_diffs end end end -- cgit v1.2.1 From a9ec2427bc641b1cddec7ce9d428a74cc155ad40 Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Wed, 10 May 2017 14:47:43 -0400 Subject: Add pipeline id to Last Pipeline cell link. --- app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml | 1 + spec/features/projects/pipeline_schedules_spec.rb | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml index 1406868488f..075ecee4343 100644 --- a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml +++ b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml @@ -10,6 +10,7 @@ .status-icon-container{ class: "ci-status-icon-#{pipeline_schedule.last_pipeline.status}" } = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline_schedule.last_pipeline.id) do = ci_icon_for_status(pipeline_schedule.last_pipeline.status) + %span ##{pipeline_schedule.last_pipeline.id} - else None %td.next-run-cell diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb index cdac4fe2111..fe9f94db574 100644 --- a/spec/features/projects/pipeline_schedules_spec.rb +++ b/spec/features/projects/pipeline_schedules_spec.rb @@ -6,6 +6,7 @@ feature 'Pipeline Schedules', :feature do let!(:project) { create(:project) } let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project) } + let!(:pipeline) { create(:ci_pipeline, pipeline_schedule: pipeline_schedule) } let(:scope) { nil } let!(:user) { create(:user) } @@ -32,7 +33,7 @@ feature 'Pipeline Schedules', :feature do page.within('.pipeline-schedule-table-row') do expect(page).to have_content('pipeline schedule') expect(page).to have_link('master') - expect(page).to have_content('None') + expect(page).to have_link("##{pipeline.id}") end end -- cgit v1.2.1 From 22722659c233efb3b65bb35286ff07c192e3fc85 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Mon, 8 May 2017 17:58:42 +0300 Subject: fix for Follow-up from "Backport of Multiple Assignees feature --- app/controllers/concerns/issuable_actions.rb | 13 ++++++++++--- app/helpers/issuables_helper.rb | 5 +++-- app/services/issuable/bulk_update_service.rb | 14 +++++++++++++- app/services/system_note_service.rb | 2 +- app/views/shared/issuable/_assignees.html.haml | 5 ++--- .../shared/issuable/_sidebar_assignees.html.haml | 22 +++++++++++----------- .../shared/issuable/form/_issue_assignee.html.haml | 9 +++++---- doc/api/issues.md | 22 +++++++++++----------- lib/api/helpers/common_helpers.rb | 8 ++++---- spec/requests/api/issues_spec.rb | 2 +- spec/services/issuable/bulk_update_service_spec.rb | 4 ++-- spec/services/system_note_service_spec.rb | 2 +- 12 files changed, 64 insertions(+), 44 deletions(-) diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index b199f18da1e..4cf645d6341 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -60,17 +60,24 @@ module IssuableActions end def bulk_update_params - params.require(:update).permit( + permitted_keys = [ :issuable_ids, :assignee_id, :milestone_id, :state_event, :subscription_event, - assignee_ids: [], label_ids: [], add_label_ids: [], remove_label_ids: [] - ) + ] + + if resource_name == 'issue' + permitted_keys << { assignee_ids: [] } + else + permitted_keys.unshift(:assignee_id) + end + + params.require(:update).permit(permitted_keys) end def resource_name diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index fbbce6876c2..f7d0ebcb16f 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -67,9 +67,10 @@ module IssuablesHelper end def users_dropdown_label(selected_users) - if selected_users.length == 0 + case selected_users.length + when 0 "Unassigned" - elsif selected_users.length == 1 + when 1 selected_users[0].name else "#{selected_users[0].name} + #{selected_users.length - 1} more" diff --git a/app/services/issuable/bulk_update_service.rb b/app/services/issuable/bulk_update_service.rb index 40ff9b8b867..5d42a89fced 100644 --- a/app/services/issuable/bulk_update_service.rb +++ b/app/services/issuable/bulk_update_service.rb @@ -7,7 +7,7 @@ module Issuable ids = params.delete(:issuable_ids).split(",") items = model_class.where(id: ids) - %i(state_event milestone_id assignee_id assignee_ids add_label_ids remove_label_ids subscription_event).each do |key| + permitted_attrs(type).each do |key| params.delete(key) unless params[key].present? end @@ -26,5 +26,17 @@ module Issuable success: !items.count.zero? } end + + private + + def permitted_attrs(type) + attrs = %i(state_event milestone_id assignee_id assignee_ids add_label_ids remove_label_ids subscription_event) + + if type == 'issue' + attrs.push(:assignee_ids) + else + attrs.push(:assignee_id) + end + end end end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 174e7c6e95b..0766df50ed2 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -79,7 +79,7 @@ module SystemNoteService text_parts.join(' and ') elsif old_assignees.any? - "removed all assignees" + "removed assignee" elsif issue.assignees.any? "assigned to #{issue.assignees.map(&:to_reference).to_sentence}" end diff --git a/app/views/shared/issuable/_assignees.html.haml b/app/views/shared/issuable/_assignees.html.haml index 36bbb1148d4..217af7c9fac 100644 --- a/app/views/shared/issuable/_assignees.html.haml +++ b/app/views/shared/issuable/_assignees.html.haml @@ -1,9 +1,8 @@ - max_render = 3 - max = [max_render, issue.assignees.length].min -- issue.assignees.each_with_index do |assignee, index| - - if index < max - = link_to_member(@project, assignee, name: false, title: "Assigned to :name") +- issue.assignees.take(max).each do |assignee| + = link_to_member(@project, assignee, name: false, title: "Assigned to :name") - if issue.assignees.length > max_render - counter = issue.assignees.length - max_render diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml index c36a45098a8..e9ce7b7ce9c 100644 --- a/app/views/shared/issuable/_sidebar_assignees.html.haml +++ b/app/views/shared/issuable/_sidebar_assignees.html.haml @@ -1,4 +1,4 @@ -- if issuable.instance_of?(Issue) +- if issuable.is_a?(Issue) #js-vue-sidebar-assignees{ data: { field: "#{issuable.to_ability_name}[assignee_ids]" } } - else .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: (issuable.assignee.name if issuable.assignee) } @@ -33,17 +33,17 @@ - options = { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_ids][]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } } - - if issuable.instance_of?(Issue) - - if issuable.assignees.length == 0 + - title = 'Select assignee' + + - if issuable.is_a?(Issue) + - unless issuable.assignees.any? = hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", 0, id: nil - - title = 'Select assignee' - options[:toggle_class] += ' js-multiselect js-save-user-data' - - options[:data][:field_name] = "#{issuable.to_ability_name}[assignee_ids][]" - - options[:data][:multi_select] = true - - options[:data]['dropdown-title'] = title - - options[:data]['dropdown-header'] = 'Assignee' - - options[:data]['max-select'] = 1 - - else - - title = 'Select assignee' + - data = { field_name: "#{issuable.to_ability_name}[assignee_ids][]" } + - data[:multi_select] = true + - data['dropdown-title'] = title + - data['dropdown-header'] = 'Assignee' + - data['max-select'] = 1 + - options[:data].merge!(data) = dropdown_tag(title, options: options) diff --git a/app/views/shared/issuable/form/_issue_assignee.html.haml b/app/views/shared/issuable/form/_issue_assignee.html.haml index c33474ac3b4..66091d95a91 100644 --- a/app/views/shared/issuable/form/_issue_assignee.html.haml +++ b/app/views/shared/issuable/form/_issue_assignee.html.haml @@ -1,8 +1,9 @@ - issue = issuable +- assignees = issue.assignees .block.assignee .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: (issuable.assignee_list) } - - if issue.assignees.any? - - issue.assignees.each do |assignee| + - if assignees.any? + - assignees.each do |assignee| = link_to_member(@project, assignee, size: 24) - else = icon('user', 'aria-hidden': 'true') @@ -12,8 +13,8 @@ - if can_edit_issuable = link_to 'Edit', '#', class: 'edit-link pull-right' .value.hide-collapsed - - if issue.assignees.any? - - issue.assignees.each do |assignee| + - if assignees.any? + - assignees.each do |assignee| = link_to_member(@project, assignee, size: 32, extra_class: 'bold') do %span.username = assignee.to_reference diff --git a/doc/api/issues.md b/doc/api/issues.md index 75794cc8d04..3f949ca5667 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -100,7 +100,7 @@ Example response: ] ``` -**Note**: `assignee` column is deprecated, it shows the first assignee only. +**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API. ## List group issues @@ -192,7 +192,7 @@ Example response: ] ``` -**Note**: `assignee` column is deprecated, it shows the first assignee only. +**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API. ## List project issues @@ -284,7 +284,7 @@ Example response: ] ``` -**Note**: `assignee` column is deprecated, it shows the first assignee only. +**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API. ## Single issue @@ -359,7 +359,7 @@ Example response: } ``` -**Note**: `assignee` column is deprecated, it shows the first assignee only. +**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API. ## New issue @@ -375,7 +375,7 @@ POST /projects/:id/issues | `title` | string | yes | The title of an issue | | `description` | string | no | The description of an issue | | `confidential` | boolean | no | Set an issue to be confidential. Default is `false`. | -| `assignee_ids` | Array[integer] | no | The ID of a user to assign issue | +| `assignee_ids` | Array[integer] | no | The ID of the users to assign issue | | `milestone_id` | integer | no | The ID of a milestone to assign issue | | `labels` | string | no | Comma-separated label names for an issue | | `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) | @@ -421,7 +421,7 @@ Example response: } ``` -**Note**: `assignee` column is deprecated, it shows the first assignee only. +**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API. ## Edit issue @@ -439,7 +439,7 @@ PUT /projects/:id/issues/:issue_iid | `title` | string | no | The title of an issue | | `description` | string | no | The description of an issue | | `confidential` | boolean | no | Updates an issue to be confidential | -| `assignee_ids` | Array[integer] | no | The ID of a user to assign the issue to | +| `assignee_ids` | Array[integer] | no | The ID of the users to assign the issue to | | `milestone_id` | integer | no | The ID of a milestone to assign the issue to | | `labels` | string | no | Comma-separated label names for an issue | | `state_event` | string | no | The state event of an issue. Set `close` to close the issue and `reopen` to reopen it | @@ -484,7 +484,7 @@ Example response: } ``` -**Note**: `assignee` column is deprecated, it shows the first assignee only. +**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API. ## Delete an issue @@ -570,7 +570,7 @@ Example response: } ``` -**Note**: `assignee` column is deprecated, it shows the first assignee only. +**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API. ## Subscribe to an issue @@ -635,7 +635,7 @@ Example response: } ``` -**Note**: `assignee` column is deprecated, it shows the first assignee only. +**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API. ## Unsubscribe from an issue @@ -757,7 +757,7 @@ Example response: } ``` -**Note**: `assignee` column is deprecated, it shows the first assignee only. +**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API. ## Set a time estimate for an issue diff --git a/lib/api/helpers/common_helpers.rb b/lib/api/helpers/common_helpers.rb index 6236fdd43ca..322624c6092 100644 --- a/lib/api/helpers/common_helpers.rb +++ b/lib/api/helpers/common_helpers.rb @@ -2,11 +2,11 @@ module API module Helpers module CommonHelpers def convert_parameters_from_legacy_format(params) - if params[:assignee_id].present? - params[:assignee_ids] = [params.delete(:assignee_id)] + params.tap do |params| + if params[:assignee_id].present? + params[:assignee_ids] = [params.delete(:assignee_id)] + end end - - params end end end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index da2b56c040b..79cac721202 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -1124,7 +1124,7 @@ describe API::Issues do end context 'CE restrictions' do - it 'updates an issue with several assignee but only one has been applied' do + it 'updates an issue with several assignees but only one has been applied' do put api("/projects/#{project.id}/issues/#{issue.iid}", user), assignee_ids: [user2.id, guest.id] diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb index 3ddd0badd39..6437d00e451 100644 --- a/spec/services/issuable/bulk_update_service_spec.rb +++ b/spec/services/issuable/bulk_update_service_spec.rb @@ -62,7 +62,7 @@ describe Issuable::BulkUpdateService, services: true do expect(result[:count]).to eq(1) end - it 'updates the assignee to the use ID passed' do + it 'updates the assignee to the user ID passed' do assignee = create(:user) project.team << [assignee, :developer] @@ -100,7 +100,7 @@ describe Issuable::BulkUpdateService, services: true do expect(result[:count]).to eq(1) end - it 'updates the assignee to the use ID passed' do + it 'updates the assignee to the user ID passed' do assignee = create(:user) project.team << [assignee, :developer] expect { bulk_update(issue, assignee_ids: [assignee.id]) } diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 516566eddef..7a9cd7553b1 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -178,7 +178,7 @@ describe SystemNoteService, services: true do end it 'builds a correct phrase when assignee removed' do - expect(build_note([assignee1], [])).to eq 'removed all assignees' + expect(build_note([assignee1], [])).to eq 'removed assignee' end it 'builds a correct phrase when assignees changed' do -- cgit v1.2.1 From 2f75bfad481496d1ceb4732a289cbcfe6fb76a36 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Thu, 11 May 2017 11:16:42 -0500 Subject: Fix overlapping lines in SVG --- app/views/shared/empty_states/icons/_pipelines_empty.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/empty_states/icons/_pipelines_empty.svg b/app/views/shared/empty_states/icons/_pipelines_empty.svg index 8119d5bebe0..7c672538097 100644 --- a/app/views/shared/empty_states/icons/_pipelines_empty.svg +++ b/app/views/shared/empty_states/icons/_pipelines_empty.svg @@ -1 +1 @@ - \ No newline at end of file + -- cgit v1.2.1 From 53d7065fe2a46522697dd485cb2e150a4c389a01 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Thu, 11 May 2017 12:01:08 -0500 Subject: Move copy button after commit sha --- app/views/projects/commits/_commit.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 8f32d2b72e5..24153b8b59a 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -37,6 +37,6 @@ .commit-actions.flex-row.hidden-xs - if commit.status(ref) = render_commit_status(commit, ref: ref) - = clipboard_button(text: commit.id, title: "Copy commit SHA to clipboard") = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent" + = clipboard_button(text: commit.id, title: "Copy commit SHA to clipboard") = link_to_browse_code(project, commit) -- cgit v1.2.1 From ecbb6e940c71e29d4a7043a42d1baa991cac0958 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 11 May 2017 12:32:47 -0500 Subject: Update copy on Create merge request dropdown --- app/views/projects/issues/_new_branch.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml index 6bc6bf76e18..dba092c8844 100644 --- a/app/views/projects/issues/_new_branch.html.haml +++ b/app/views/projects/issues/_new_branch.html.haml @@ -17,7 +17,7 @@ .description %strong Create a merge request %span - Creates a branch named after this issue and a merge request. The source branch is '#{@project.default_branch}' by default. + Creates a merge request named after this issue, with source branch created from '#{@project.default_branch}'. %li.divider.droplab-item-ignore %li{ role: 'button', data: { value: 'create-branch', 'text' => 'Create a branch' } } .menu-item @@ -26,4 +26,4 @@ .description %strong Create a branch %span - Creates a branch named after this issue. The source branch is '#{@project.default_branch}' by default. + Creates a branch named after this issue, from '#{@project.default_branch}'. -- cgit v1.2.1 From 70b926e013dd05781aaff54ea96ee27f2ef65a6a Mon Sep 17 00:00:00 2001 From: Jarka Kadlecova Date: Wed, 10 May 2017 13:13:33 +0200 Subject: Fix cross referencing for private and internal projects --- app/models/concerns/mentionable.rb | 13 ++-- .../unreleased/31978-cross-reference-fix.yml | 4 ++ spec/features/issues/notes_on_issues_spec.rb | 77 ++++++++++++++++++++++ 3 files changed, 88 insertions(+), 6 deletions(-) create mode 100644 changelogs/unreleased/31978-cross-reference-fix.yml create mode 100644 spec/features/issues/notes_on_issues_spec.rb diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index 6eddeab515e..c034bf9cbc0 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -44,14 +44,15 @@ module Mentionable end def all_references(current_user = nil, extractor: nil) + @extractors ||= {} + # Use custom extractor if it's passed in the function parameters. if extractor - @extractor = extractor + @extractors[current_user] = extractor else - @extractor ||= Gitlab::ReferenceExtractor. - new(project, current_user) + extractor = @extractors[current_user] ||= Gitlab::ReferenceExtractor.new(project, current_user) - @extractor.reset_memoized_values + extractor.reset_memoized_values end self.class.mentionable_attrs.each do |attr, options| @@ -62,10 +63,10 @@ module Mentionable skip_project_check: skip_project_check? ) - @extractor.analyze(text, options) + extractor.analyze(text, options) end - @extractor + extractor end def mentioned_users(current_user = nil) diff --git a/changelogs/unreleased/31978-cross-reference-fix.yml b/changelogs/unreleased/31978-cross-reference-fix.yml new file mode 100644 index 00000000000..fbcb3d5d482 --- /dev/null +++ b/changelogs/unreleased/31978-cross-reference-fix.yml @@ -0,0 +1,4 @@ +--- +title: Fix cross referencing for private and internal projects +merge_request: 11243 +author: diff --git a/spec/features/issues/notes_on_issues_spec.rb b/spec/features/issues/notes_on_issues_spec.rb new file mode 100644 index 00000000000..a4035324d2b --- /dev/null +++ b/spec/features/issues/notes_on_issues_spec.rb @@ -0,0 +1,77 @@ +require 'spec_helper' + +describe 'Create notes on issues', :js, :feature do + let(:user) { create(:user) } + + shared_examples 'notes with reference' do + let(:issue) { create(:issue, project: project) } + let(:note_text) { "Check #{mention.to_reference}" } + + before do + project.team << [user, :developer] + login_as(user) + visit namespace_project_issue_path(project.namespace, project, issue) + + fill_in 'note[note]', with: note_text + click_button 'Comment' + + wait_for_ajax + end + + it 'creates a note with reference and cross references the issue' do + page.within('div#notes li.note div.note-text') do + expect(page).to have_content(note_text) + expect(page.find('a')).to have_content(mention.to_reference) + end + + find('div#notes li.note div.note-text a').click + + page.within('div#notes li.note .system-note-message') do + expect(page).to have_content('mentioned in issue') + expect(page.find('a')).to have_content(issue.to_reference) + end + end + end + + context 'mentioning issue on a private project' do + it_behaves_like 'notes with reference' do + let(:project) { create(:project, :private) } + let(:mention) { create(:issue, project: project) } + end + end + + context 'mentioning issue on an internal project' do + it_behaves_like 'notes with reference' do + let(:project) { create(:project, :internal) } + let(:mention) { create(:issue, project: project) } + end + end + + context 'mentioning issue on a public project' do + it_behaves_like 'notes with reference' do + let(:project) { create(:project, :public) } + let(:mention) { create(:issue, project: project) } + end + end + + context 'mentioning merge request on a private project' do + it_behaves_like 'notes with reference' do + let(:project) { create(:project, :private) } + let(:mention) { create(:merge_request, source_project: project) } + end + end + + context 'mentioning merge request on an internal project' do + it_behaves_like 'notes with reference' do + let(:project) { create(:project, :internal) } + let(:mention) { create(:merge_request, source_project: project) } + end + end + + context 'mentioning merge request on a public project' do + it_behaves_like 'notes with reference' do + let(:project) { create(:project, :public) } + let(:mention) { create(:merge_request, source_project: project) } + end + end +end -- cgit v1.2.1 From 55b6232c74fbfdfaeed096c8df955a2017086824 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Thu, 11 May 2017 19:11:03 +0000 Subject: Removed unneeded false from poll_spec mockServiceCall call --- spec/javascripts/lib/utils/poll_spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/javascripts/lib/utils/poll_spec.js b/spec/javascripts/lib/utils/poll_spec.js index 65e89fb10b4..22f30191ab9 100644 --- a/spec/javascripts/lib/utils/poll_spec.js +++ b/spec/javascripts/lib/utils/poll_spec.js @@ -34,7 +34,7 @@ describe('Poll', () => { }); it('calls the success callback when no header for interval is provided', (done) => { - mockServiceCall(service, { status: 200 }, false); + mockServiceCall(service, { status: 200 }); new Poll({ resource: service, -- cgit v1.2.1 From 77f2ec4fc37bedead3622bc0d626ac5459ed7316 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 11 May 2017 12:14:47 -0700 Subject: Remove unnecessary cache definition in rake karma test Closes #32119 --- .gitlab-ci.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 88d536fa9b3..7fbfda4a5a8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -408,9 +408,6 @@ rake gitlab:assets:compile: - webpack-report/ rake karma: - cache: - paths: - - vendor/ruby stage: test <<: *use-pg <<: *dedicated-runner -- cgit v1.2.1 From 9f4fa4cdc7e32f63d7afcd608f1af86dbc4ae477 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 11 May 2017 15:38:30 -0500 Subject: Fix displaying a repository without a readme --- app/models/repository.rb | 4 ++-- spec/models/repository_spec.rb | 16 +++++++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 11163ec77e9..b1563bfba8b 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -517,8 +517,8 @@ class Repository cache_method :avatar def readme - if head = tree(:head) - ReadmeBlob.new(head.readme, self) + if readme = tree(:head)&.readme + ReadmeBlob.new(readme, self) end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 00a9f2abeb9..61b748429d7 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1659,15 +1659,25 @@ describe Repository, models: true do describe '#readme', caching: true do context 'with a non-existing repository' do it 'returns nil' do - expect(repository).to receive(:tree).with(:head).and_return(nil) + allow(repository).to receive(:tree).with(:head).and_return(nil) expect(repository.readme).to be_nil end end context 'with an existing repository' do - it 'returns the README' do - expect(repository.readme).to be_an_instance_of(ReadmeBlob) + context 'when no README exists' do + it 'returns nil' do + allow_any_instance_of(Tree).to receive(:readme).and_return(nil) + + expect(repository.readme).to be_nil + end + end + + context 'when a README exists' do + it 'returns the README' do + expect(repository.readme).to be_an_instance_of(ReadmeBlob) + end end end end -- cgit v1.2.1 From 5d7d509840fc3941e5977ef02e948e7124925372 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Wed, 10 May 2017 23:19:30 +0300 Subject: MRWidget: Fix target branch link. --- .../components/states/mr_widget_locked.js | 2 +- .../states/mr_widget_merge_when_pipeline_succeeds.js | 2 +- .../vue_merge_request_widget/stores/mr_widget_store.js | 1 + .../vue_mr_widget/components/mr_widget_header_spec.js | 13 ++++++++++--- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_locked.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_locked.js index e3c27dfb76d..0bd31731a0b 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_locked.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_locked.js @@ -16,7 +16,7 @@ export default { The changes will be merged into {{mr.targetBranch}} - + .

    diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js index bcdbedcd46b..419d174f3ff 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js @@ -87,7 +87,7 @@ export default { :href="mr.targetBranchPath" class="label-branch"> {{mr.targetBranch}} - + .

    The source branch will be removed. diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index faafeae5c5b..cc8446972db 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -43,6 +43,7 @@ export default class MergeRequestStore { this.mergeUserId = data.merge_user_id; this.currentUserId = gon.current_user_id; this.sourceBranchPath = data.source_branch_path; + this.targetBranchCommitsPath = data.target_branch_commits_path; this.sourceBranchLink = data.source_branch_with_namespace_link; this.mergeError = data.merge_error; this.targetBranchPath = data.target_branch_commits_path; diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js index 48f816c8460..c07cd344b07 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js @@ -48,10 +48,12 @@ describe('MRWidgetHeader', () => { describe('template', () => { let vm; let el; + const sourceBranchPath = '/foo/bar/mr-widget-refactor'; const mr = { divergedCommitsCount: 12, sourceBranch: 'mr-widget-refactor', - sourceBranchLink: '/foo/bar/mr-widget-refactor', + sourceBranchLink: `mr-widget-refactor`, + targetBranchCommitsPath: 'foo/bar/commits-path', targetBranch: 'master', isOpen: true, emailPatchesPath: '/mr/email-patches', @@ -65,8 +67,13 @@ describe('MRWidgetHeader', () => { it('should render template elements correctly', () => { expect(el.classList.contains('mr-source-target')).toBeTruthy(); - expect(el.querySelectorAll('.label-branch')[0].textContent).toContain(mr.sourceBranch); - expect(el.querySelectorAll('.label-branch')[1].textContent).toContain(mr.targetBranch); + const sourceBranchLink = el.querySelectorAll('.label-branch')[0]; + const targetBranchLink = el.querySelectorAll('.label-branch')[1]; + + expect(sourceBranchLink.textContent).toContain(mr.sourceBranch); + expect(targetBranchLink.textContent).toContain(mr.targetBranch); + expect(sourceBranchLink.querySelector('a').getAttribute('href')).toEqual(sourceBranchPath); + expect(targetBranchLink.querySelector('a').getAttribute('href')).toEqual(mr.targetBranchCommitsPath); expect(el.querySelector('.diverged-commits-count').textContent).toContain('12 commits behind'); expect(el.textContent).toContain('Check out branch'); -- cgit v1.2.1 From ab98f8b5b1fb323ded965cdb352a504525dd0703 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Thu, 11 May 2017 13:57:03 -0700 Subject: Fix redirect message for groups and users --- app/controllers/concerns/routable_actions.rb | 8 ++++---- spec/controllers/groups_controller_spec.rb | 8 ++++++-- spec/controllers/projects_controller_spec.rb | 8 ++++++-- spec/controllers/users_controller_spec.rb | 12 ++++++++---- 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/app/controllers/concerns/routable_actions.rb b/app/controllers/concerns/routable_actions.rb index d4ab6782444..afd110adcad 100644 --- a/app/controllers/concerns/routable_actions.rb +++ b/app/controllers/concerns/routable_actions.rb @@ -4,7 +4,7 @@ module RoutableActions def find_routable!(routable_klass, requested_full_path, extra_authorization_proc: nil) routable = routable_klass.find_by_full_path(requested_full_path, follow_redirects: request.get?) - if routable_authorized?(routable_klass, routable, extra_authorization_proc) + if routable_authorized?(routable, extra_authorization_proc) ensure_canonical_path(routable, requested_full_path) routable else @@ -13,8 +13,8 @@ module RoutableActions end end - def routable_authorized?(routable_klass, routable, extra_authorization_proc) - action = :"read_#{routable_klass.to_s.underscore}" + def routable_authorized?(routable, extra_authorization_proc) + action = :"read_#{routable.class.to_s.underscore}" return false unless can?(current_user, action, routable) if extra_authorization_proc @@ -30,7 +30,7 @@ module RoutableActions canonical_path = routable.full_path if canonical_path != requested_path if canonical_path.casecmp(requested_path) != 0 - flash[:notice] = "Project '#{requested_path}' was moved to '#{canonical_path}'. Please update any links and bookmarks that may still have the old path." + flash[:notice] = "#{routable.class.to_s.titleize} '#{requested_path}' was moved to '#{canonical_path}'. Please update any links and bookmarks that may still have the old path." end redirect_to request.original_url.sub(requested_path, canonical_path) end diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index df8ea225814..15dae3231ca 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -101,7 +101,7 @@ describe GroupsController do get :issues, id: redirect_route.path expect(response).to redirect_to(issues_group_path(group.to_param)) - expect(controller).to set_flash[:notice].to(/moved/) + expect(controller).to set_flash[:notice].to(group_moved_message(redirect_route, group)) end end end @@ -146,7 +146,7 @@ describe GroupsController do get :merge_requests, id: redirect_route.path expect(response).to redirect_to(merge_requests_group_path(group.to_param)) - expect(controller).to set_flash[:notice].to(/moved/) + expect(controller).to set_flash[:notice].to(group_moved_message(redirect_route, group)) end end end @@ -249,4 +249,8 @@ describe GroupsController do end end end + + def group_moved_message(redirect_route, group) + "Group '#{redirect_route.path}' was moved to '#{group.full_path}'. Please update any links and bookmarks that may still have the old path." + end end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index e46ef447df2..e230944d52e 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -227,7 +227,7 @@ describe ProjectsController do get :show, namespace_id: 'foo', id: 'bar' expect(response).to redirect_to(public_project) - expect(controller).to set_flash[:notice].to(/moved/) + expect(controller).to set_flash[:notice].to(project_moved_message(redirect_route, public_project)) end end end @@ -473,7 +473,7 @@ describe ProjectsController do get :refs, namespace_id: 'foo', id: 'bar' expect(response).to redirect_to(refs_namespace_project_path(namespace_id: public_project.namespace, id: public_project)) - expect(controller).to set_flash[:notice].to(/moved/) + expect(controller).to set_flash[:notice].to(project_moved_message(redirect_route, public_project)) end end end @@ -487,4 +487,8 @@ describe ProjectsController do expect(JSON.parse(response.body).keys).to match_array(%w(body references)) end end + + def project_moved_message(redirect_route, project) + "Project '#{redirect_route.path}' was moved to '#{project.full_path}'. Please update any links and bookmarks that may still have the old path." + end end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 74c5aa44ba9..1d61719f1d0 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -83,7 +83,7 @@ describe UsersController do get :show, username: redirect_route.path expect(response).to redirect_to(user) - expect(controller).to set_flash[:notice].to(/moved/) + expect(controller).to set_flash[:notice].to(user_moved_message(redirect_route, user)) end end @@ -162,7 +162,7 @@ describe UsersController do get :calendar, username: redirect_route.path expect(response).to redirect_to(user_calendar_path(user)) - expect(controller).to set_flash[:notice].to(/moved/) + expect(controller).to set_flash[:notice].to(user_moved_message(redirect_route, user)) end end end @@ -216,7 +216,7 @@ describe UsersController do get :calendar_activities, username: redirect_route.path expect(response).to redirect_to(user_calendar_activities_path(user)) - expect(controller).to set_flash[:notice].to(/moved/) + expect(controller).to set_flash[:notice].to(user_moved_message(redirect_route, user)) end end end @@ -270,7 +270,7 @@ describe UsersController do get :snippets, username: redirect_route.path expect(response).to redirect_to(user_snippets_path(user)) - expect(controller).to set_flash[:notice].to(/moved/) + expect(controller).to set_flash[:notice].to(user_moved_message(redirect_route, user)) end end end @@ -320,4 +320,8 @@ describe UsersController do end end end + + def user_moved_message(redirect_route, user) + "User '#{redirect_route.path}' was moved to '#{user.full_path}'. Please update any links and bookmarks that may still have the old path." + end end -- cgit v1.2.1 From 997a12c99fee116e063aa00ce6b5e71770ca7cf7 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 11 May 2017 17:54:47 -0500 Subject: add sha-mismatch state to mr-widget to prevent accidental merges when branch unknowingly changes --- .../components/states/mr_widget_sha_mismatch.js | 16 ++++++++++++++++ .../javascripts/vue_merge_request_widget/dependencies.js | 1 + .../vue_merge_request_widget/mr_widget_options.js | 2 ++ .../vue_merge_request_widget/stores/get_state_key.js | 2 ++ .../vue_merge_request_widget/stores/mr_widget_store.js | 2 ++ .../vue_merge_request_widget/stores/state_maps.js | 1 + 6 files changed, 24 insertions(+) create mode 100644 app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js new file mode 100644 index 00000000000..79f8ef408e6 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js @@ -0,0 +1,16 @@ +export default { + name: 'MRWidgetSHAMismatch', + template: ` +

    + + + The source branch HEAD has recently changed. Please reload the page and review the changes before merging. + +
    + `, +}; diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js index b2eb32ead5f..bfe30ee4c08 100644 --- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js +++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js @@ -27,6 +27,7 @@ export { default as NothingToMergeState } from './components/states/mr_widget_no export { default as MissingBranchState } from './components/states/mr_widget_missing_branch'; export { default as NotAllowedState } from './components/states/mr_widget_not_allowed'; export { default as ReadyToMergeState } from './components/states/mr_widget_ready_to_merge'; +export { default as SHAMismatchState } from './components/states/mr_widget_sha_mismatch'; export { default as UnresolvedDiscussionsState } from './components/states/mr_widget_unresolved_discussions'; export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked'; export { default as PipelineFailedState } from './components/states/mr_widget_pipeline_failed'; diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js index 7c6c2d21714..5452e19bd8e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js @@ -16,6 +16,7 @@ import { MissingBranchState, NotAllowedState, ReadyToMergeState, + SHAMismatchState, UnresolvedDiscussionsState, PipelineBlockedState, PipelineFailedState, @@ -203,6 +204,7 @@ export default { 'mr-widget-not-allowed': NotAllowedState, 'mr-widget-missing-branch': MissingBranchState, 'mr-widget-ready-to-merge': ReadyToMergeState, + 'mr-widget-sha-mismatch': SHAMismatchState, 'mr-widget-squash-before-merge': SquashBeforeMerge, 'mr-widget-checking': CheckingState, 'mr-widget-unresolved-discussions': UnresolvedDiscussionsState, diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js index fee4113f3c8..fb78ea92da1 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js @@ -21,6 +21,8 @@ export default function deviseState(data) { return 'unresolvedDiscussions'; } else if (this.isPipelineBlocked) { return 'pipelineBlocked'; + } else if (this.hasSHAChanged) { + return 'shaMismatch'; } else if (this.canBeMerged) { return 'readyToMerge'; } diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index faafeae5c5b..05e67706983 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -4,6 +4,7 @@ import { getStateKey } from '../dependencies'; export default class MergeRequestStore { constructor(data) { + this.startingSha = data.diff_head_sha; this.setData(data); } @@ -67,6 +68,7 @@ export default class MergeRequestStore { this.canMerge = !!data.merge_path; this.canCreateIssue = currentUser.can_create_issue || false; this.canCancelAutomaticMerge = !!data.cancel_merge_when_pipeline_succeeds_path; + this.hasSHAChanged = this.sha !== this.startingSha; this.canBeMerged = data.can_be_merged || false; // Cherry-pick and Revert actions related diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js index 625d7a01c65..605dd3a1ff4 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js @@ -16,6 +16,7 @@ const stateToComponentMap = { mergeWhenPipelineSucceeds: 'mr-widget-merge-when-pipeline-succeeds', failedToMerge: 'mr-widget-failed-to-merge', autoMergeFailed: 'mr-widget-auto-merge-failed', + shaMismatch: 'mr-widget-sha-mismatch', }; const statesToShowHelpWidget = [ -- cgit v1.2.1 From 9bcf95bda038592c132255f641b6f0ea5b3f21c6 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 11 May 2017 18:11:43 -0500 Subject: add test for sha-mismatch state component --- .../components/states/mr_widget_sha_mismatch_spec.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js new file mode 100644 index 00000000000..5fb1d69a8b3 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js @@ -0,0 +1,16 @@ +import Vue from 'vue'; +import shaMismatchComponent from '~/vue_merge_request_widget/components/states/mr_widget_sha_mismatch'; + +describe('MRWidgetSHAMismatch', () => { + describe('template', () => { + const Component = Vue.extend(shaMismatchComponent); + const vm = new Component({ + el: document.createElement('div'), + }); + it('should have correct elements', () => { + expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy(); + expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy(); + expect(vm.$el.innerText).toContain('The source branch HEAD has recently changed. Please reload the page and review the changes before merging.'); + }); + }); +}); -- cgit v1.2.1 From 65c2f43fd31bdd2659d12e8eb5169ad2f32d19ba Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 11 May 2017 18:41:40 -0500 Subject: =?UTF-8?q?Use=20.trigger(=E2=80=98click=E2=80=99)=20instead=20of?= =?UTF-8?q?=20.click?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is to improve reliability on mentioned tests --- spec/features/boards/sidebar_spec.rb | 2 +- spec/features/dashboard/milestone_filter_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index 11ef8e1f61b..1238647d3f3 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -158,7 +158,7 @@ describe 'Issue Boards', feature: true, js: true do end page.within(first('.board')) do - find('.card:nth-child(2)').click + find('.card:nth-child(2)').trigger('click') end page.within('.assignee') do diff --git a/spec/features/dashboard/milestone_filter_spec.rb b/spec/features/dashboard/milestone_filter_spec.rb index 628627f70d4..e9ca7fd50e7 100644 --- a/spec/features/dashboard/milestone_filter_spec.rb +++ b/spec/features/dashboard/milestone_filter_spec.rb @@ -43,7 +43,7 @@ describe 'Dashboard > milestone filter', feature: true, js: true do end it 'should not change active Milestone unless clicked' do - find(milestone_select).click + find(milestone_select).trigger('click') expect(find('.dropdown-content')).to have_selector('a.is-active', count: 1) -- cgit v1.2.1 From f19ec4e2ae2045ab9f137273195f18adfd173705 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 11 May 2017 18:47:27 -0500 Subject: ensure the correct state component is loaded when hasSHAChanged == true --- spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js b/spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js index ee944f4d4e5..9a331d99865 100644 --- a/spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js +++ b/spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js @@ -25,6 +25,9 @@ describe('getStateKey', () => { context.canBeMerged = true; expect(bound()).toEqual('readyToMerge'); + context.hasSHAChanged = true; + expect(bound()).toEqual('shaMismatch'); + context.isPipelineBlocked = true; expect(bound()).toEqual('pipelineBlocked'); -- cgit v1.2.1 From 0945fa0430143a58b3ebdaaac5180c9abf4bd3f7 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 11 May 2017 18:48:15 -0500 Subject: ensure hasSHAChanged == true when setData includes a different SHA from constructor --- .../vue_mr_widget/stores/mr_widget_store_spec.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js diff --git a/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js b/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js new file mode 100644 index 00000000000..56dd0198ae2 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js @@ -0,0 +1,22 @@ +import MergeRequestStore from '~/vue_merge_request_widget/stores/mr_widget_store'; +import mockData from '../mock_data'; + +describe('MergeRequestStore', () => { + describe('setData', () => { + let store; + + beforeEach(() => { + store = new MergeRequestStore(mockData); + }); + + it('should set hasSHAChanged when the diff SHA changes', () => { + store.setData({ ...mockData, diff_head_sha: 'a-different-string' }); + expect(store.hasSHAChanged).toBe(true); + }); + + it('should not set hasSHAChanged when other data changes', () => { + store.setData({ ...mockData, work_in_progress: !mockData.work_in_progress }); + expect(store.hasSHAChanged).toBe(false); + }); + }); +}); -- cgit v1.2.1 From b6122aa9c7f370a6565649e6f5eb0c735210d64b Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 11 May 2017 18:50:59 -0500 Subject: add CHANGELOG.md entry for !11316 --- changelogs/unreleased/32178-prevent-merge-on-sha-change.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/32178-prevent-merge-on-sha-change.yml diff --git a/changelogs/unreleased/32178-prevent-merge-on-sha-change.yml b/changelogs/unreleased/32178-prevent-merge-on-sha-change.yml new file mode 100644 index 00000000000..d3208973de6 --- /dev/null +++ b/changelogs/unreleased/32178-prevent-merge-on-sha-change.yml @@ -0,0 +1,4 @@ +--- +title: Add state to MR widget that prevent merges when branch changes after page load +merge_request: 11316 +author: -- cgit v1.2.1 From bc916754fb6a30cdd09f7f2ec7fc74785c339150 Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Thu, 11 May 2017 21:09:59 -0400 Subject: Remove 'New issue' button when issues search returns no results. --- app/views/shared/empty_states/_issues.html.haml | 1 - ...ton-on-no-results-page-after-search-doesn-t-go-to-correct-form.yml | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/31384-new-issue-button-on-no-results-page-after-search-doesn-t-go-to-correct-form.yml diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml index 12d99c3ab4b..046b127f73c 100644 --- a/app/views/shared/empty_states/_issues.html.haml +++ b/app/views/shared/empty_states/_issues.html.haml @@ -20,4 +20,3 @@ - else .text-center %h4 There are no issues to show. - = link_to 'New issue', button_path, class: 'btn btn-new', title: 'New issue', id: 'new_issue_link' diff --git a/changelogs/unreleased/31384-new-issue-button-on-no-results-page-after-search-doesn-t-go-to-correct-form.yml b/changelogs/unreleased/31384-new-issue-button-on-no-results-page-after-search-doesn-t-go-to-correct-form.yml new file mode 100644 index 00000000000..8d586616e07 --- /dev/null +++ b/changelogs/unreleased/31384-new-issue-button-on-no-results-page-after-search-doesn-t-go-to-correct-form.yml @@ -0,0 +1,4 @@ +--- +title: Remove 'New issue' button when issues search returns no results. +merge_request: !11263 +author: -- cgit v1.2.1 From 80ec3f2715f121d3be41e4ce3b5bffc107322925 Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Thu, 11 May 2017 22:40:56 -0400 Subject: Ensure issues are enabled on the project. --- spec/features/issues_spec.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 5285dda361b..fdd78600a1d 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -573,6 +573,8 @@ describe 'Issues', feature: true do end describe 'new issue' do + let!(:issue) { create(:issue, project: project) } + context 'by unauthenticated user' do before do logout -- cgit v1.2.1 From 5bed3390aa9f9df96552ef0a2c1911bcfc36974e Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 11 May 2017 22:31:20 -0700 Subject: Fix failing backup filename spec for RCs See http://rubular.com/r/9oI7K8b773 for more details. --- spec/tasks/gitlab/backup_rake_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index df2f2ce95e6..aee926877e0 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -352,7 +352,7 @@ describe 'gitlab:app namespace rake task' do end it 'name has human readable time' do - expect(@backup_tar).to match(/\d+_\d{4}_\d{2}_\d{2}_\d+\.\d+\.\d+(-pre)?_gitlab_backup.tar$/) + expect(@backup_tar).to match(/\d+_\d{4}_\d{2}_\d{2}_\d+\.\d+\.\d+(-pre|-rc\d+)?_gitlab_backup.tar$/) end end end # gitlab:app namespace -- cgit v1.2.1 From dd70776f67a7377deeaf369d59fd09e630f2f7ed Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Fri, 12 May 2017 08:40:15 +0300 Subject: MRWidget: Use targetBranchPath in everywhere. --- .../vue_merge_request_widget/components/mr_widget_header.js | 2 +- .../vue_merge_request_widget/components/states/mr_widget_closed.js | 2 +- .../javascripts/vue_merge_request_widget/stores/mr_widget_store.js | 1 - spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js | 4 ++-- .../vue_mr_widget/components/states/mr_widget_closed_spec.js | 4 ++-- 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js index 4a1fd881169..fb648d66a30 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js @@ -83,7 +83,7 @@ export default { :title="isBranchTitleLong(mr.targetBranch) ? mr.targetBranch : ''" data-placement="bottom"> + :href="mr.targetBranchPath"> {{mr.targetBranch}} diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js index 7e66441e5ff..fc2e42c6821 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js @@ -20,7 +20,7 @@ export default {

    The changes were not merged into {{mr.targetBranch}}.

    diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index cc8446972db..faafeae5c5b 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -43,7 +43,6 @@ export default class MergeRequestStore { this.mergeUserId = data.merge_user_id; this.currentUserId = gon.current_user_id; this.sourceBranchPath = data.source_branch_path; - this.targetBranchCommitsPath = data.target_branch_commits_path; this.sourceBranchLink = data.source_branch_with_namespace_link; this.mergeError = data.merge_error; this.targetBranchPath = data.target_branch_commits_path; diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js index c07cd344b07..7f3eea7d2e5 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js @@ -53,7 +53,7 @@ describe('MRWidgetHeader', () => { divergedCommitsCount: 12, sourceBranch: 'mr-widget-refactor', sourceBranchLink: `mr-widget-refactor`, - targetBranchCommitsPath: 'foo/bar/commits-path', + targetBranchPath: 'foo/bar/commits-path', targetBranch: 'master', isOpen: true, emailPatchesPath: '/mr/email-patches', @@ -73,7 +73,7 @@ describe('MRWidgetHeader', () => { expect(sourceBranchLink.textContent).toContain(mr.sourceBranch); expect(targetBranchLink.textContent).toContain(mr.targetBranch); expect(sourceBranchLink.querySelector('a').getAttribute('href')).toEqual(sourceBranchPath); - expect(targetBranchLink.querySelector('a').getAttribute('href')).toEqual(mr.targetBranchCommitsPath); + expect(targetBranchLink.querySelector('a').getAttribute('href')).toEqual(mr.targetBranchPath); expect(el.querySelector('.diverged-commits-count').textContent).toContain('12 commits behind'); expect(el.textContent).toContain('Check out branch'); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js index 78a70725e94..47303d1e80f 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js @@ -3,7 +3,7 @@ import closedComponent from '~/vue_merge_request_widget/components/states/mr_wid const mr = { targetBranch: 'good-branch', - targetBranchCommitsPath: '/good-branch', + targetBranchPath: '/good-branch', closedBy: { name: 'Fatih Acet', username: 'fatihacet', @@ -44,7 +44,7 @@ describe('MRWidgetClosed', () => { expect(el.querySelector('h4').textContent).toContain('Closed by'); expect(el.querySelector('h4').textContent).toContain(mr.closedBy.name); expect(el.textContent).toContain('The changes were not merged into'); - expect(el.querySelector('.label-branch').getAttribute('href')).toEqual(mr.targetBranchCommitsPath); + expect(el.querySelector('.label-branch').getAttribute('href')).toEqual(mr.targetBranchPath); expect(el.querySelector('.label-branch').textContent).toContain(mr.targetBranch); }); }); -- cgit v1.2.1 From 05278b2fda7843a877a8d5190aacb70b9212df3d Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Fri, 12 May 2017 08:15:28 +0000 Subject: Import export users select --- app/assets/javascripts/dispatcher.js | 15 +- app/assets/javascripts/issuable_context.js | 2 +- app/assets/javascripts/issuable_form.js | 3 +- app/assets/javascripts/todos.js | 3 +- app/assets/javascripts/users_select.js | 1186 +++++++++++------------ app/views/import/fogbugz/new_user_map.html.haml | 3 - app/views/shared/issuable/_filter.html.haml | 1 - app/views/shared/issuable/_search_bar.html.haml | 1 - 8 files changed, 609 insertions(+), 605 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 43ad127a4db..1a791395d6f 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -14,7 +14,6 @@ /* global NotificationsForm */ /* global TreeView */ /* global NotificationsDropdown */ -/* global UsersSelect */ /* global GroupAvatar */ /* global LineHighlighter */ /* global ProjectFork */ @@ -52,6 +51,7 @@ import ShortcutsWiki from './shortcuts_wiki'; import Pipelines from './pipelines'; import BlobViewer from './blob/viewer/index'; import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select'; +import UsersSelect from './users_select'; const ShortcutsBlob = require('./shortcuts_blob'); @@ -113,6 +113,7 @@ const ShortcutsBlob = require('./shortcuts_blob'); case 'projects:boards:show': case 'projects:boards:index': shortcut_handler = new ShortcutsNavigation(); + new UsersSelect(); break; case 'projects:builds:show': new Build(); @@ -127,6 +128,7 @@ const ShortcutsBlob = require('./shortcuts_blob'); prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_', }); shortcut_handler = new ShortcutsNavigation(); + new UsersSelect(); break; case 'projects:issues:show': new Issue(); @@ -139,6 +141,10 @@ const ShortcutsBlob = require('./shortcuts_blob'); new Milestone(); new Sidebar(); break; + case 'groups:issues': + case 'groups:merge_requests': + new UsersSelect(); + break; case 'dashboard:todos:index': new gl.Todos(); break; @@ -223,6 +229,10 @@ const ShortcutsBlob = require('./shortcuts_blob'); case 'dashboard:activity': new gl.Activities(); break; + case 'dashboard:issues': + case 'dashboard:merge_requests': + new UsersSelect(); + break; case 'projects:commit:show': new Commit(); new gl.Diff(); @@ -377,6 +387,9 @@ const ShortcutsBlob = require('./shortcuts_blob'); new LineHighlighter(); new BlobViewer(); break; + case 'import:fogbugz:new_user_map': + new UsersSelect(); + break; } switch (path.first()) { case 'sessions': diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js index 834b98e8601..4520e990e6f 100644 --- a/app/assets/javascripts/issuable_context.js +++ b/app/assets/javascripts/issuable_context.js @@ -1,8 +1,8 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, max-len */ -/* global UsersSelect */ /* global bp */ import Cookies from 'js-cookie'; +import UsersSelect from './users_select'; (function() { this.IssuableContext = (function() { diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index 544fc91876a..4310663e0b6 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -1,11 +1,12 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, max-len */ /* global GitLab */ -/* global UsersSelect */ /* global ZenMode */ /* global Autosave */ /* global dateFormat */ /* global Pikaday */ +import UsersSelect from './users_select'; + (function() { this.IssuableForm = (function() { IssuableForm.prototype.issueMoveConfirmMsg = 'Are you sure you want to move this issue to another project?'; diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/todos.js index 8be58023c84..7230946b484 100644 --- a/app/assets/javascripts/todos.js +++ b/app/assets/javascripts/todos.js @@ -1,5 +1,6 @@ /* eslint-disable class-methods-use-this, no-unneeded-ternary, quote-props */ -/* global UsersSelect */ + +import UsersSelect from './users_select'; class Todos { constructor() { diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index 452578d5b98..8119a8cd000 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -5,655 +5,649 @@ // TODO: remove eventHub hack after code splitting refactor window.emitSidebarEvent = window.emitSidebarEvent || $.noop; -(function() { - const slice = [].slice; - - this.UsersSelect = (function() { - function UsersSelect(currentUser, els) { - var $els; - this.users = this.users.bind(this); - this.user = this.user.bind(this); - this.usersPath = "/autocomplete/users.json"; - this.userPath = "/autocomplete/users/:id.json"; - if (currentUser != null) { - if (typeof currentUser === 'object') { - this.currentUser = currentUser; - } else { - this.currentUser = JSON.parse(currentUser); +function UsersSelect(currentUser, els) { + var $els; + this.users = this.users.bind(this); + this.user = this.user.bind(this); + this.usersPath = "/autocomplete/users.json"; + this.userPath = "/autocomplete/users/:id.json"; + if (currentUser != null) { + if (typeof currentUser === 'object') { + this.currentUser = currentUser; + } else { + this.currentUser = JSON.parse(currentUser); + } + } + + $els = $(els); + + if (!els) { + $els = $('.js-user-search'); + } + + $els.each((function(_this) { + return function(i, dropdown) { + var options = {}; + var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, defaultNullUser, firstUser, issueURL, selectedId, selectedIdDefault, showAnyUser, showNullUser, showMenuAbove; + $dropdown = $(dropdown); + options.projectId = $dropdown.data('project-id'); + options.groupId = $dropdown.data('group-id'); + options.showCurrentUser = $dropdown.data('current-user'); + options.todoFilter = $dropdown.data('todo-filter'); + options.todoStateFilter = $dropdown.data('todo-state-filter'); + showNullUser = $dropdown.data('null-user'); + defaultNullUser = $dropdown.data('null-user-default'); + showMenuAbove = $dropdown.data('showMenuAbove'); + showAnyUser = $dropdown.data('any-user'); + firstUser = $dropdown.data('first-user'); + options.authorId = $dropdown.data('author-id'); + defaultLabel = $dropdown.data('default-label'); + issueURL = $dropdown.data('issueUpdate'); + $selectbox = $dropdown.closest('.selectbox'); + $block = $selectbox.closest('.block'); + abilityName = $dropdown.data('ability-name'); + $value = $block.find('.value'); + $collapsedSidebar = $block.find('.sidebar-collapsed-user'); + $loading = $block.find('.block-loading').fadeOut(); + selectedIdDefault = (defaultNullUser && showNullUser) ? 0 : null; + selectedId = $dropdown.data('selected') || selectedIdDefault; + + const assignYourself = function () { + const unassignedSelected = $dropdown.closest('.selectbox') + .find(`input[name='${$dropdown.data('field-name')}'][value=0]`); + + if (unassignedSelected) { + unassignedSelected.remove(); } - } - $els = $(els); + // Save current selected user to the DOM + const input = document.createElement('input'); + input.type = 'hidden'; + input.name = $dropdown.data('field-name'); + + const currentUserInfo = $dropdown.data('currentUserInfo'); + + if (currentUserInfo) { + input.value = currentUserInfo.id; + input.dataset.meta = currentUserInfo.name; + } else if (_this.currentUser) { + input.value = _this.currentUser.id; + } - if (!els) { - $els = $('.js-user-search'); + if ($selectbox) { + $dropdown.parent().before(input); + } else { + $dropdown.after(input); + } + }; + + if ($block[0]) { + $block[0].addEventListener('assignYourself', assignYourself); } - $els.each((function(_this) { - return function(i, dropdown) { - var options = {}; - var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, defaultNullUser, firstUser, issueURL, selectedId, selectedIdDefault, showAnyUser, showNullUser, showMenuAbove; - $dropdown = $(dropdown); - options.projectId = $dropdown.data('project-id'); - options.groupId = $dropdown.data('group-id'); - options.showCurrentUser = $dropdown.data('current-user'); - options.todoFilter = $dropdown.data('todo-filter'); - options.todoStateFilter = $dropdown.data('todo-state-filter'); - showNullUser = $dropdown.data('null-user'); - defaultNullUser = $dropdown.data('null-user-default'); - showMenuAbove = $dropdown.data('showMenuAbove'); - showAnyUser = $dropdown.data('any-user'); - firstUser = $dropdown.data('first-user'); - options.authorId = $dropdown.data('author-id'); - defaultLabel = $dropdown.data('default-label'); - issueURL = $dropdown.data('issueUpdate'); - $selectbox = $dropdown.closest('.selectbox'); - $block = $selectbox.closest('.block'); - abilityName = $dropdown.data('ability-name'); - $value = $block.find('.value'); - $collapsedSidebar = $block.find('.sidebar-collapsed-user'); - $loading = $block.find('.block-loading').fadeOut(); - selectedIdDefault = (defaultNullUser && showNullUser) ? 0 : null; - selectedId = $dropdown.data('selected') || selectedIdDefault; - - const assignYourself = function () { - const unassignedSelected = $dropdown.closest('.selectbox') - .find(`input[name='${$dropdown.data('field-name')}'][value=0]`); - - if (unassignedSelected) { - unassignedSelected.remove(); - } + const getSelectedUserInputs = function() { + return $selectbox + .find(`input[name="${$dropdown.data('field-name')}"]`); + }; + + const getSelected = function() { + return getSelectedUserInputs() + .map((index, input) => parseInt(input.value, 10)) + .get(); + }; + + const checkMaxSelect = function() { + const maxSelect = $dropdown.data('max-select'); + if (maxSelect) { + const selected = getSelected(); + + if (selected.length > maxSelect) { + const firstSelectedId = selected[0]; + const firstSelected = $dropdown.closest('.selectbox') + .find(`input[name='${$dropdown.data('field-name')}'][value=${firstSelectedId}]`); + + firstSelected.remove(); + emitSidebarEvent('sidebar.removeAssignee', { + id: firstSelectedId, + }); + } + } + }; + + const getMultiSelectDropdownTitle = function(selectedUser, isSelected) { + const selectedUsers = getSelected() + .filter(u => u !== 0); + + const firstUser = getSelectedUserInputs() + .map((index, input) => ({ + name: input.dataset.meta, + value: parseInt(input.value, 10), + })) + .filter(u => u.id !== 0) + .get(0); + + if (selectedUsers.length === 0) { + return 'Unassigned'; + } else if (selectedUsers.length === 1) { + return firstUser.name; + } else if (isSelected) { + const otherSelected = selectedUsers.filter(s => s !== selectedUser.id); + return `${selectedUser.name} + ${otherSelected.length} more`; + } else { + return `${firstUser.name} + ${selectedUsers.length - 1} more`; + } + }; - // Save current selected user to the DOM - const input = document.createElement('input'); - input.type = 'hidden'; - input.name = $dropdown.data('field-name'); + $('.assign-to-me-link').on('click', (e) => { + e.preventDefault(); + $(e.currentTarget).hide(); - const currentUserInfo = $dropdown.data('currentUserInfo'); + if ($dropdown.data('multiSelect')) { + assignYourself(); + checkMaxSelect(); - if (currentUserInfo) { - input.value = currentUserInfo.id; - input.dataset.meta = currentUserInfo.name; - } else if (_this.currentUser) { - input.value = _this.currentUser.id; - } + const currentUserInfo = $dropdown.data('currentUserInfo'); + $dropdown.find('.dropdown-toggle-text').text(getMultiSelectDropdownTitle(currentUserInfo)).removeClass('is-default'); + } else { + const $input = $(`input[name="${$dropdown.data('field-name')}"]`); + $input.val(gon.current_user_id); + selectedId = $input.val(); + $dropdown.find('.dropdown-toggle-text').text(gon.current_user_fullname).removeClass('is-default'); + } + }); - if ($selectbox) { - $dropdown.parent().before(input); - } else { - $dropdown.after(input); - } - }; + $block.on('click', '.js-assign-yourself', (e) => { + e.preventDefault(); + return assignTo(_this.currentUser.id); + }); - if ($block[0]) { - $block[0].addEventListener('assignYourself', assignYourself); + assignTo = function(selected) { + var data; + data = {}; + data[abilityName] = {}; + data[abilityName].assignee_id = selected != null ? selected : null; + $loading.removeClass('hidden').fadeIn(); + $dropdown.trigger('loading.gl.dropdown'); + + return $.ajax({ + type: 'PUT', + dataType: 'json', + url: issueURL, + data: data + }).done(function(data) { + var user; + $dropdown.trigger('loaded.gl.dropdown'); + $loading.fadeOut(); + if (data.assignee) { + user = { + name: data.assignee.name, + username: data.assignee.username, + avatar: data.assignee.avatar_url + }; + } else { + user = { + name: 'Unassigned', + username: '', + avatar: '' + }; } - - const getSelectedUserInputs = function() { - return $selectbox - .find(`input[name="${$dropdown.data('field-name')}"]`); - }; - - const getSelected = function() { - return getSelectedUserInputs() - .map((index, input) => parseInt(input.value, 10)) - .get(); - }; - - const checkMaxSelect = function() { - const maxSelect = $dropdown.data('max-select'); - if (maxSelect) { - const selected = getSelected(); - - if (selected.length > maxSelect) { - const firstSelectedId = selected[0]; - const firstSelected = $dropdown.closest('.selectbox') - .find(`input[name='${$dropdown.data('field-name')}'][value=${firstSelectedId}]`); - - firstSelected.remove(); - emitSidebarEvent('sidebar.removeAssignee', { - id: firstSelectedId, - }); + $value.html(assigneeTemplate(user)); + $collapsedSidebar.attr('title', user.name).tooltip('fixTitle'); + return $collapsedSidebar.html(collapsedAssigneeTemplate(user)); + }); + }; + collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <% } else { %> <% } %>'); + assigneeTemplate = _.template('<% if (username) { %> <% if( avatar ) { %> <% } %> <%- name %> @<%- username %> <% } else { %> No assignee - assign yourself <% } %>'); + return $dropdown.glDropdown({ + showMenuAbove: showMenuAbove, + data: function(term, callback) { + var isAuthorFilter; + isAuthorFilter = $('.js-author-search'); + return _this.users(term, options, function(users) { + // GitLabDropdownFilter returns this.instance + // GitLabDropdownRemote returns this.options.instance + const glDropdown = this.instance || this.options.instance; + glDropdown.options.processData(term, users, callback); + }.bind(this)); + }, + processData: function(term, users, callback) { + let anyUser; + let index; + let j; + let len; + let name; + let obj; + let showDivider; + if (term.length === 0) { + showDivider = 0; + if (firstUser) { + // Move current user to the front of the list + for (index = j = 0, len = users.length; j < len; index = (j += 1)) { + obj = users[index]; + if (obj.username === firstUser) { + users.splice(index, 1); + users.unshift(obj); + break; + } } } - }; - - const getMultiSelectDropdownTitle = function(selectedUser, isSelected) { - const selectedUsers = getSelected() - .filter(u => u !== 0); - - const firstUser = getSelectedUserInputs() - .map((index, input) => ({ - name: input.dataset.meta, - value: parseInt(input.value, 10), - })) - .filter(u => u.id !== 0) - .get(0); - - if (selectedUsers.length === 0) { - return 'Unassigned'; - } else if (selectedUsers.length === 1) { - return firstUser.name; - } else if (isSelected) { - const otherSelected = selectedUsers.filter(s => s !== selectedUser.id); - return `${selectedUser.name} + ${otherSelected.length} more`; - } else { - return `${firstUser.name} + ${selectedUsers.length - 1} more`; + if (showNullUser) { + showDivider += 1; + users.unshift({ + beforeDivider: true, + name: 'Unassigned', + id: 0 + }); + } + if (showAnyUser) { + showDivider += 1; + name = showAnyUser; + if (name === true) { + name = 'Any User'; + } + anyUser = { + beforeDivider: true, + name: name, + id: null + }; + users.unshift(anyUser); } - }; - - $('.assign-to-me-link').on('click', (e) => { - e.preventDefault(); - $(e.currentTarget).hide(); - - if ($dropdown.data('multiSelect')) { - assignYourself(); - checkMaxSelect(); - const currentUserInfo = $dropdown.data('currentUserInfo'); - $dropdown.find('.dropdown-toggle-text').text(getMultiSelectDropdownTitle(currentUserInfo)).removeClass('is-default'); - } else { - const $input = $(`input[name="${$dropdown.data('field-name')}"]`); - $input.val(gon.current_user_id); - selectedId = $input.val(); - $dropdown.find('.dropdown-toggle-text').text(gon.current_user_fullname).removeClass('is-default'); + if (showDivider) { + users.splice(showDivider, 0, 'divider'); } - }); - $block.on('click', '.js-assign-yourself', (e) => { - e.preventDefault(); - return assignTo(_this.currentUser.id); - }); + if ($dropdown.hasClass('js-multiselect')) { + const selected = getSelected().filter(i => i !== 0); - assignTo = function(selected) { - var data; - data = {}; - data[abilityName] = {}; - data[abilityName].assignee_id = selected != null ? selected : null; - $loading.removeClass('hidden').fadeIn(); - $dropdown.trigger('loading.gl.dropdown'); - - return $.ajax({ - type: 'PUT', - dataType: 'json', - url: issueURL, - data: data - }).done(function(data) { - var user; - $dropdown.trigger('loaded.gl.dropdown'); - $loading.fadeOut(); - if (data.assignee) { - user = { - name: data.assignee.name, - username: data.assignee.username, - avatar: data.assignee.avatar_url - }; - } else { - user = { - name: 'Unassigned', - username: '', - avatar: '' - }; - } - $value.html(assigneeTemplate(user)); - $collapsedSidebar.attr('title', user.name).tooltip('fixTitle'); - return $collapsedSidebar.html(collapsedAssigneeTemplate(user)); - }); - }; - collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <% } else { %> <% } %>'); - assigneeTemplate = _.template('<% if (username) { %> <% if( avatar ) { %> <% } %> <%- name %> @<%- username %> <% } else { %> No assignee - assign yourself <% } %>'); - return $dropdown.glDropdown({ - showMenuAbove: showMenuAbove, - data: function(term, callback) { - var isAuthorFilter; - isAuthorFilter = $('.js-author-search'); - return _this.users(term, options, function(users) { - // GitLabDropdownFilter returns this.instance - // GitLabDropdownRemote returns this.options.instance - const glDropdown = this.instance || this.options.instance; - glDropdown.options.processData(term, users, callback); - }.bind(this)); - }, - processData: function(term, users, callback) { - let anyUser; - let index; - let j; - let len; - let name; - let obj; - let showDivider; - if (term.length === 0) { - showDivider = 0; - if (firstUser) { - // Move current user to the front of the list - for (index = j = 0, len = users.length; j < len; index = (j += 1)) { - obj = users[index]; - if (obj.username === firstUser) { - users.splice(index, 1); - users.unshift(obj); - break; - } - } - } - if (showNullUser) { + if (selected.length > 0) { + if ($dropdown.data('dropdown-header')) { showDivider += 1; - users.unshift({ - beforeDivider: true, - name: 'Unassigned', - id: 0 + users.splice(showDivider, 0, { + header: $dropdown.data('dropdown-header'), }); } - if (showAnyUser) { - showDivider += 1; - name = showAnyUser; - if (name === true) { - name = 'Any User'; - } - anyUser = { - beforeDivider: true, - name: name, - id: null - }; - users.unshift(anyUser); - } - if (showDivider) { - users.splice(showDivider, 0, 'divider'); - } + const selectedUsers = users + .filter(u => selected.indexOf(u.id) !== -1) + .sort((a, b) => a.name > b.name); - if ($dropdown.hasClass('js-multiselect')) { - const selected = getSelected().filter(i => i !== 0); + users = users.filter(u => selected.indexOf(u.id) === -1); - if (selected.length > 0) { - if ($dropdown.data('dropdown-header')) { - showDivider += 1; - users.splice(showDivider, 0, { - header: $dropdown.data('dropdown-header'), - }); - } - - const selectedUsers = users - .filter(u => selected.indexOf(u.id) !== -1) - .sort((a, b) => a.name > b.name); + selectedUsers.forEach((selectedUser) => { + showDivider += 1; + users.splice(showDivider, 0, selectedUser); + }); - users = users.filter(u => selected.indexOf(u.id) === -1); + users.splice(showDivider + 1, 0, 'divider'); + } + } + } - selectedUsers.forEach((selectedUser) => { - showDivider += 1; - users.splice(showDivider, 0, selectedUser); - }); + callback(users); + if (showMenuAbove) { + $dropdown.data('glDropdown').positionMenuAbove(); + } + }, + filterable: true, + filterRemote: true, + search: { + fields: ['name', 'username'] + }, + selectable: true, + fieldName: $dropdown.data('field-name'), + toggleLabel: function(selected, el, glDropdown) { + const inputValue = glDropdown.filterInput.val(); + + if (this.multiSelect && inputValue === '') { + // Remove non-users from the fullData array + const users = glDropdown.filteredFullData(); + const callback = glDropdown.parseData.bind(glDropdown); + + // Update the data model + this.processData(inputValue, users, callback); + } - users.splice(showDivider + 1, 0, 'divider'); - } - } - } + if (this.multiSelect) { + return getMultiSelectDropdownTitle(selected, $(el).hasClass('is-active')); + } - callback(users); - if (showMenuAbove) { - $dropdown.data('glDropdown').positionMenuAbove(); - } - }, - filterable: true, - filterRemote: true, - search: { - fields: ['name', 'username'] - }, - selectable: true, - fieldName: $dropdown.data('field-name'), - toggleLabel: function(selected, el, glDropdown) { - const inputValue = glDropdown.filterInput.val(); - - if (this.multiSelect && inputValue === '') { - // Remove non-users from the fullData array - const users = glDropdown.filteredFullData(); - const callback = glDropdown.parseData.bind(glDropdown); - - // Update the data model - this.processData(inputValue, users, callback); - } + if (selected && 'id' in selected && $(el).hasClass('is-active')) { + $dropdown.find('.dropdown-toggle-text').removeClass('is-default'); + if (selected.text) { + return selected.text; + } else { + return selected.name; + } + } else { + $dropdown.find('.dropdown-toggle-text').addClass('is-default'); + return defaultLabel; + } + }, + defaultLabel: defaultLabel, + hidden: function(e) { + if ($dropdown.hasClass('js-multiselect')) { + emitSidebarEvent('sidebar.saveAssignees'); + } - if (this.multiSelect) { - return getMultiSelectDropdownTitle(selected, $(el).hasClass('is-active')); - } + if (!$dropdown.data('always-show-selectbox')) { + $selectbox.hide(); - if (selected && 'id' in selected && $(el).hasClass('is-active')) { - $dropdown.find('.dropdown-toggle-text').removeClass('is-default'); - if (selected.text) { - return selected.text; - } else { - return selected.name; - } - } else { - $dropdown.find('.dropdown-toggle-text').addClass('is-default'); - return defaultLabel; - } - }, - defaultLabel: defaultLabel, - hidden: function(e) { - if ($dropdown.hasClass('js-multiselect')) { - emitSidebarEvent('sidebar.saveAssignees'); - } + // Recalculate where .value is because vue might have changed it + $block = $selectbox.closest('.block'); + $value = $block.find('.value'); + // display:block overrides the hide-collapse rule + $value.css('display', ''); + } + }, + multiSelect: $dropdown.hasClass('js-multiselect'), + inputMeta: $dropdown.data('input-meta'), + clicked: function(options) { + const { $el, e, isMarking } = options; + const user = options.selectedObj; + + if ($dropdown.hasClass('js-multiselect')) { + const isActive = $el.hasClass('is-active'); + const previouslySelected = $dropdown.closest('.selectbox') + .find("input[name='" + ($dropdown.data('field-name')) + "'][value!=0]"); + + // Enables support for limiting the number of users selected + // Automatically removes the first on the list if more users are selected + checkMaxSelect(); + + if (user.beforeDivider && user.name.toLowerCase() === 'unassigned') { + // Unassigned selected + previouslySelected.each((index, element) => { + const id = parseInt(element.value, 10); + element.remove(); + }); + emitSidebarEvent('sidebar.removeAllAssignees'); + } else if (isActive) { + // user selected + emitSidebarEvent('sidebar.addAssignee', user); - if (!$dropdown.data('always-show-selectbox')) { - $selectbox.hide(); + // Remove unassigned selection (if it was previously selected) + const unassignedSelected = $dropdown.closest('.selectbox') + .find("input[name='" + ($dropdown.data('field-name')) + "'][value=0]"); - // Recalculate where .value is because vue might have changed it - $block = $selectbox.closest('.block'); - $value = $block.find('.value'); - // display:block overrides the hide-collapse rule - $value.css('display', ''); + if (unassignedSelected) { + unassignedSelected.remove(); + } + } else { + if (previouslySelected.length === 0) { + // Select unassigned because there is no more selected users + this.addInput($dropdown.data('field-name'), 0, {}); } - }, - multiSelect: $dropdown.hasClass('js-multiselect'), - inputMeta: $dropdown.data('input-meta'), - clicked: function(options) { - const { $el, e, isMarking } = options; - const user = options.selectedObj; - - if ($dropdown.hasClass('js-multiselect')) { - const isActive = $el.hasClass('is-active'); - const previouslySelected = $dropdown.closest('.selectbox') - .find("input[name='" + ($dropdown.data('field-name')) + "'][value!=0]"); - - // Enables support for limiting the number of users selected - // Automatically removes the first on the list if more users are selected - checkMaxSelect(); - - if (user.beforeDivider && user.name.toLowerCase() === 'unassigned') { - // Unassigned selected - previouslySelected.each((index, element) => { - const id = parseInt(element.value, 10); - element.remove(); - }); - emitSidebarEvent('sidebar.removeAllAssignees'); - } else if (isActive) { - // user selected - emitSidebarEvent('sidebar.addAssignee', user); - // Remove unassigned selection (if it was previously selected) - const unassignedSelected = $dropdown.closest('.selectbox') - .find("input[name='" + ($dropdown.data('field-name')) + "'][value=0]"); + // User unselected + emitSidebarEvent('sidebar.removeAssignee', user); + } - if (unassignedSelected) { - unassignedSelected.remove(); - } - } else { - if (previouslySelected.length === 0) { - // Select unassigned because there is no more selected users - this.addInput($dropdown.data('field-name'), 0, {}); - } + if (getSelected().find(u => u === gon.current_user_id)) { + $('.assign-to-me-link').hide(); + } else { + $('.assign-to-me-link').show(); + } + } - // User unselected - emitSidebarEvent('sidebar.removeAssignee', user); - } + var isIssueIndex, isMRIndex, page, selected; + page = $('body').data('page'); + isIssueIndex = page === 'projects:issues:index'; + isMRIndex = (page === page && page === 'projects:merge_requests:index'); + if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) { + e.preventDefault(); - if (getSelected().find(u => u === gon.current_user_id)) { - $('.assign-to-me-link').hide(); - } else { - $('.assign-to-me-link').show(); - } - } + const isSelecting = (user.id !== selectedId); + selectedId = isSelecting ? user.id : selectedIdDefault; - var isIssueIndex, isMRIndex, page, selected; - page = $('body').data('page'); - isIssueIndex = page === 'projects:issues:index'; - isMRIndex = (page === page && page === 'projects:merge_requests:index'); - if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) { - e.preventDefault(); + if (selectedId === gon.current_user_id) { + $('.assign-to-me-link').hide(); + } else { + $('.assign-to-me-link').show(); + } + return; + } + if ($el.closest('.add-issues-modal').length) { + gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id; + } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { + return Issuable.filterResults($dropdown.closest('form')); + } else if ($dropdown.hasClass('js-filter-submit')) { + return $dropdown.closest('form').submit(); + } else if (!$dropdown.hasClass('js-multiselect')) { + selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").val(); + return assignTo(selected); + } + }, + id: function (user) { + return user.id; + }, + opened: function(e) { + const $el = $(e.currentTarget); + if ($dropdown.hasClass('js-issue-board-sidebar')) { + selectedId = parseInt($dropdown[0].dataset.selected, 10) || selectedIdDefault; + } + $el.find('.is-active').removeClass('is-active'); - const isSelecting = (user.id !== selectedId); - selectedId = isSelecting ? user.id : selectedIdDefault; + function highlightSelected(id) { + $el.find(`li[data-user-id="${id}"] .dropdown-menu-user-link`).addClass('is-active'); + } - if (selectedId === gon.current_user_id) { - $('.assign-to-me-link').hide(); - } else { - $('.assign-to-me-link').show(); - } - return; - } - if ($el.closest('.add-issues-modal').length) { - gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id; - } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { - return Issuable.filterResults($dropdown.closest('form')); - } else if ($dropdown.hasClass('js-filter-submit')) { - return $dropdown.closest('form').submit(); - } else if (!$dropdown.hasClass('js-multiselect')) { - selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").val(); - return assignTo(selected); - } - }, - id: function (user) { - return user.id; - }, - opened: function(e) { - const $el = $(e.currentTarget); - if ($dropdown.hasClass('js-issue-board-sidebar')) { - selectedId = parseInt($dropdown[0].dataset.selected, 10) || selectedIdDefault; - } - $el.find('.is-active').removeClass('is-active'); + if ($selectbox[0]) { + getSelected().forEach(selectedId => highlightSelected(selectedId)); + } else { + highlightSelected(selectedId); + } + }, + updateLabel: $dropdown.data('dropdown-title'), + renderRow: function(user) { + var avatar, img, listClosingTags, listWithName, listWithUserName, username; + username = user.username ? "@" + user.username : ""; + avatar = user.avatar_url ? user.avatar_url : false; - function highlightSelected(id) { - $el.find(`li[data-user-id="${id}"] .dropdown-menu-user-link`).addClass('is-active'); - } + let selected = user.id === parseInt(selectedId, 10); - if ($selectbox[0]) { - getSelected().forEach(selectedId => highlightSelected(selectedId)); - } else { - highlightSelected(selectedId); - } - }, - updateLabel: $dropdown.data('dropdown-title'), - renderRow: function(user) { - var avatar, img, listClosingTags, listWithName, listWithUserName, username; - username = user.username ? "@" + user.username : ""; - avatar = user.avatar_url ? user.avatar_url : false; + if (this.multiSelect) { + const fieldName = this.fieldName; + const field = $dropdown.closest('.selectbox').find("input[name='" + fieldName + "'][value='" + user.id + "']"); - let selected = user.id === parseInt(selectedId, 10); + if (field.length) { + selected = true; + } + } - if (this.multiSelect) { - const fieldName = this.fieldName; - const field = $dropdown.closest('.selectbox').find("input[name='" + fieldName + "'][value='" + user.id + "']"); + img = ""; + if (user.beforeDivider != null) { + `
  • ${user.name}
  • `; + } else { + if (avatar) { + img = ""; + } + } - if (field.length) { - selected = true; + return ` +
  • + + ${img} + + ${user.name} + + ${username ? `${username}` : ''} + +
  • + `; + } + }); + }; + })(this)); + $('.ajax-users-select').each((function(_this) { + return function(i, select) { + var firstUser, showAnyUser, showEmailUser, showNullUser; + var options = {}; + options.skipLdap = $(select).hasClass('skip_ldap'); + options.projectId = $(select).data('project-id'); + options.groupId = $(select).data('group-id'); + options.showCurrentUser = $(select).data('current-user'); + options.pushCodeToProtectedBranches = $(select).data('push-code-to-protected-branches'); + options.authorId = $(select).data('author-id'); + options.skipUsers = $(select).data('skip-users'); + showNullUser = $(select).data('null-user'); + showAnyUser = $(select).data('any-user'); + showEmailUser = $(select).data('email-user'); + firstUser = $(select).data('first-user'); + return $(select).select2({ + placeholder: "Search for a user", + multiple: $(select).hasClass('multiselect'), + minimumInputLength: 0, + query: function(query) { + return _this.users(query.term, options, function(users) { + var anyUser, data, emailUser, index, j, len, name, nullUser, obj, ref; + data = { + results: users + }; + if (query.term.length === 0) { + if (firstUser) { + // Move current user to the front of the list + ref = data.results; + for (index = j = 0, len = ref.length; j < len; index = (j += 1)) { + obj = ref[index]; + if (obj.username === firstUser) { + data.results.splice(index, 1); + data.results.unshift(obj); + break; + } } } - - img = ""; - if (user.beforeDivider != null) { - `
  • ${user.name}
  • `; - } else { - if (avatar) { - img = ""; + if (showNullUser) { + nullUser = { + name: 'Unassigned', + id: 0 + }; + data.results.unshift(nullUser); + } + if (showAnyUser) { + name = showAnyUser; + if (name === true) { + name = 'Any User'; } + anyUser = { + name: name, + id: null + }; + data.results.unshift(anyUser); } - - return ` -
  • - - ${img} - - ${user.name} - - ${username ? `${username}` : ''} - -
  • - `; } - }); - }; - })(this)); - $('.ajax-users-select').each((function(_this) { - return function(i, select) { - var firstUser, showAnyUser, showEmailUser, showNullUser; - var options = {}; - options.skipLdap = $(select).hasClass('skip_ldap'); - options.projectId = $(select).data('project-id'); - options.groupId = $(select).data('group-id'); - options.showCurrentUser = $(select).data('current-user'); - options.pushCodeToProtectedBranches = $(select).data('push-code-to-protected-branches'); - options.authorId = $(select).data('author-id'); - options.skipUsers = $(select).data('skip-users'); - showNullUser = $(select).data('null-user'); - showAnyUser = $(select).data('any-user'); - showEmailUser = $(select).data('email-user'); - firstUser = $(select).data('first-user'); - return $(select).select2({ - placeholder: "Search for a user", - multiple: $(select).hasClass('multiselect'), - minimumInputLength: 0, - query: function(query) { - return _this.users(query.term, options, function(users) { - var anyUser, data, emailUser, index, j, len, name, nullUser, obj, ref; - data = { - results: users - }; - if (query.term.length === 0) { - if (firstUser) { - // Move current user to the front of the list - ref = data.results; - for (index = j = 0, len = ref.length; j < len; index = (j += 1)) { - obj = ref[index]; - if (obj.username === firstUser) { - data.results.splice(index, 1); - data.results.unshift(obj); - break; - } - } - } - if (showNullUser) { - nullUser = { - name: 'Unassigned', - id: 0 - }; - data.results.unshift(nullUser); - } - if (showAnyUser) { - name = showAnyUser; - if (name === true) { - name = 'Any User'; - } - anyUser = { - name: name, - id: null - }; - data.results.unshift(anyUser); - } - } - if (showEmailUser && data.results.length === 0 && query.term.match(/^[^@]+@[^@]+$/)) { - var trimmed = query.term.trim(); - emailUser = { - name: "Invite \"" + query.term + "\"", - username: trimmed, - id: trimmed - }; - data.results.unshift(emailUser); - } - return query.callback(data); - }); - }, - initSelection: function() { - var args; - args = 1 <= arguments.length ? slice.call(arguments, 0) : []; - return _this.initSelection.apply(_this, args); - }, - formatResult: function() { - var args; - args = 1 <= arguments.length ? slice.call(arguments, 0) : []; - return _this.formatResult.apply(_this, args); - }, - formatSelection: function() { - var args; - args = 1 <= arguments.length ? slice.call(arguments, 0) : []; - return _this.formatSelection.apply(_this, args); - }, - dropdownCssClass: "ajax-users-dropdown", - // we do not want to escape markup since we are displaying html in results - escapeMarkup: function(m) { - return m; + if (showEmailUser && data.results.length === 0 && query.term.match(/^[^@]+@[^@]+$/)) { + var trimmed = query.term.trim(); + emailUser = { + name: "Invite \"" + query.term + "\"", + username: trimmed, + id: trimmed + }; + data.results.unshift(emailUser); } + return query.callback(data); }); - }; - })(this)); - } - - UsersSelect.prototype.initSelection = function(element, callback) { - var id, nullUser; - id = $(element).val(); - if (id === "0") { - nullUser = { - name: 'Unassigned' - }; - return callback(nullUser); - } else if (id !== "") { - return this.user(id, callback); - } - }; - - UsersSelect.prototype.formatResult = function(user) { - var avatar; - if (user.avatar_url) { - avatar = user.avatar_url; - } else { - avatar = gon.default_avatar_url; - } - return "
    " + user.name + "
    " + (user.username || "") + "
    "; - }; - - UsersSelect.prototype.formatSelection = function(user) { - return user.name; - }; - - UsersSelect.prototype.user = function(user_id, callback) { - if (!/^\d+$/.test(user_id)) { - return false; - } - - var url; - url = this.buildUrl(this.userPath); - url = url.replace(':id', user_id); - return $.ajax({ - url: url, - dataType: "json" - }).done(function(user) { - return callback(user); - }); - }; - - // Return users list. Filtered by query - // Only active users retrieved - UsersSelect.prototype.users = function(query, options, callback) { - var url; - url = this.buildUrl(this.usersPath); - return $.ajax({ - url: url, - data: { - search: query, - per_page: 20, - active: true, - project_id: options.projectId || null, - group_id: options.groupId || null, - skip_ldap: options.skipLdap || null, - todo_filter: options.todoFilter || null, - todo_state_filter: options.todoStateFilter || null, - current_user: options.showCurrentUser || null, - push_code_to_protected_branches: options.pushCodeToProtectedBranches || null, - author_id: options.authorId || null, - skip_users: options.skipUsers || null }, - dataType: "json" - }).done(function(users) { - return callback(users); + initSelection: function() { + var args; + args = 1 <= arguments.length ? [].slice.call(arguments, 0) : []; + return _this.initSelection.apply(_this, args); + }, + formatResult: function() { + var args; + args = 1 <= arguments.length ? [].slice.call(arguments, 0) : []; + return _this.formatResult.apply(_this, args); + }, + formatSelection: function() { + var args; + args = 1 <= arguments.length ? [].slice.call(arguments, 0) : []; + return _this.formatSelection.apply(_this, args); + }, + dropdownCssClass: "ajax-users-dropdown", + // we do not want to escape markup since we are displaying html in results + escapeMarkup: function(m) { + return m; + } }); }; - - UsersSelect.prototype.buildUrl = function(url) { - if (gon.relative_url_root != null) { - url = gon.relative_url_root.replace(/\/$/, '') + url; - } - return url; + })(this)); +} + +UsersSelect.prototype.initSelection = function(element, callback) { + var id, nullUser; + id = $(element).val(); + if (id === "0") { + nullUser = { + name: 'Unassigned' }; - - return UsersSelect; - })(); -}).call(window); + return callback(nullUser); + } else if (id !== "") { + return this.user(id, callback); + } +}; + +UsersSelect.prototype.formatResult = function(user) { + var avatar; + if (user.avatar_url) { + avatar = user.avatar_url; + } else { + avatar = gon.default_avatar_url; + } + return "
    " + user.name + "
    " + (user.username || "") + "
    "; +}; + +UsersSelect.prototype.formatSelection = function(user) { + return user.name; +}; + +UsersSelect.prototype.user = function(user_id, callback) { + if (!/^\d+$/.test(user_id)) { + return false; + } + + var url; + url = this.buildUrl(this.userPath); + url = url.replace(':id', user_id); + return $.ajax({ + url: url, + dataType: "json" + }).done(function(user) { + return callback(user); + }); +}; + +// Return users list. Filtered by query +// Only active users retrieved +UsersSelect.prototype.users = function(query, options, callback) { + var url; + url = this.buildUrl(this.usersPath); + return $.ajax({ + url: url, + data: { + search: query, + per_page: 20, + active: true, + project_id: options.projectId || null, + group_id: options.groupId || null, + skip_ldap: options.skipLdap || null, + todo_filter: options.todoFilter || null, + todo_state_filter: options.todoStateFilter || null, + current_user: options.showCurrentUser || null, + push_code_to_protected_branches: options.pushCodeToProtectedBranches || null, + author_id: options.authorId || null, + skip_users: options.skipUsers || null + }, + dataType: "json" + }).done(function(users) { + return callback(users); + }); +}; + +UsersSelect.prototype.buildUrl = function(url) { + if (gon.relative_url_root != null) { + url = gon.relative_url_root.replace(/\/$/, '') + url; + } + return url; +}; + +export default UsersSelect; diff --git a/app/views/import/fogbugz/new_user_map.html.haml b/app/views/import/fogbugz/new_user_map.html.haml index 9999a4362c6..c52a515226e 100644 --- a/app/views/import/fogbugz/new_user_map.html.haml +++ b/app/views/import/fogbugz/new_user_map.html.haml @@ -46,6 +46,3 @@ .form-actions = submit_tag 'Continue to the next step', class: 'btn btn-create' - -:javascript - new UsersSelect(); diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 1a12f110945..6cd03f028a9 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -71,7 +71,6 @@ = render 'shared/labels_row', labels: @labels :javascript - new UsersSelect(); new LabelsSelect(); new MilestoneSelect(); new IssueStatusSelect(); diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index f7b87171573..622e2f33eea 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -150,7 +150,6 @@ - unless type === :boards_modal :javascript - new UsersSelect(); new LabelsSelect(); new MilestoneSelect(); new IssueStatusSelect(); -- cgit v1.2.1 From 92ca24fc50b72f4ba7da1008ddb234fbadaf6737 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 12 May 2017 09:36:43 +0100 Subject: Added model specs --- spec/models/protected_branch/merge_access_level_spec.rb | 5 +++++ spec/models/protected_branch/push_access_level_spec.rb | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 spec/models/protected_branch/merge_access_level_spec.rb create mode 100644 spec/models/protected_branch/push_access_level_spec.rb diff --git a/spec/models/protected_branch/merge_access_level_spec.rb b/spec/models/protected_branch/merge_access_level_spec.rb new file mode 100644 index 00000000000..1e7242e9fa8 --- /dev/null +++ b/spec/models/protected_branch/merge_access_level_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe ProtectedBranch::MergeAccessLevel, :models do + it { is_expected.to validate_inclusion_of(:access_level).in_array([Gitlab::Access::MASTER, Gitlab::Access::DEVELOPER, Gitlab::Access::NO_ACCESS]) } +end diff --git a/spec/models/protected_branch/push_access_level_spec.rb b/spec/models/protected_branch/push_access_level_spec.rb new file mode 100644 index 00000000000..de68351198c --- /dev/null +++ b/spec/models/protected_branch/push_access_level_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe ProtectedBranch::PushAccessLevel, :models do + it { is_expected.to validate_inclusion_of(:access_level).in_array([Gitlab::Access::MASTER, Gitlab::Access::DEVELOPER, Gitlab::Access::NO_ACCESS]) } +end -- cgit v1.2.1 From f6c4ccd1f234e1b5bab8f7ffbd98b3d1092b4873 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Fri, 12 May 2017 10:16:33 +0300 Subject: Backport FileFinder from EE --- lib/gitlab/file_finder.rb | 32 ++++++++++++++++++++++++++ lib/gitlab/project_search_results.rb | 18 +-------------- spec/lib/gitlab/file_finder_spec.rb | 21 +++++++++++++++++ spec/lib/gitlab/project_search_results_spec.rb | 2 +- 4 files changed, 55 insertions(+), 18 deletions(-) create mode 100644 lib/gitlab/file_finder.rb create mode 100644 spec/lib/gitlab/file_finder_spec.rb diff --git a/lib/gitlab/file_finder.rb b/lib/gitlab/file_finder.rb new file mode 100644 index 00000000000..093d9ed8092 --- /dev/null +++ b/lib/gitlab/file_finder.rb @@ -0,0 +1,32 @@ +# This class finds files in a repository by name and content +# the result is joined and sorted by file name +module Gitlab + class FileFinder + BATCH_SIZE = 100 + + attr_reader :project, :ref + + def initialize(project, ref) + @project = project + @ref = ref + end + + def find(query) + blobs = project.repository.search_files_by_content(query, ref).first(BATCH_SIZE) + found_file_names = Set.new + + results = blobs.map do |blob| + blob = Gitlab::ProjectSearchResults.parse_search_result(blob) + found_file_names << blob.filename + + [blob.filename, blob] + end + + project.repository.search_files_by_name(query, ref).first(BATCH_SIZE).each do |filename| + results << [filename, OpenStruct.new(ref: ref)] unless found_file_names.include?(filename) + end + + results.sort_by(&:first) + end + end +end diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb index 47cfe412715..561aa9e162c 100644 --- a/lib/gitlab/project_search_results.rb +++ b/lib/gitlab/project_search_results.rb @@ -84,23 +84,7 @@ module Gitlab def blobs return [] unless Ability.allowed?(@current_user, :download_code, @project) - @blobs ||= begin - blobs = project.repository.search_files_by_content(query, repository_ref).first(100) - found_file_names = Set.new - - results = blobs.map do |blob| - blob = self.class.parse_search_result(blob) - found_file_names << blob.filename - - [blob.filename, blob] - end - - project.repository.search_files_by_name(query, repository_ref).first(100).each do |filename| - results << [filename, nil] unless found_file_names.include?(filename) - end - - results.sort_by(&:first) - end + @blobs ||= Gitlab::FileFinder.new(project, repository_ref).find(query) end def wiki_blobs diff --git a/spec/lib/gitlab/file_finder_spec.rb b/spec/lib/gitlab/file_finder_spec.rb new file mode 100644 index 00000000000..5a32ffd462c --- /dev/null +++ b/spec/lib/gitlab/file_finder_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Gitlab::FileFinder, lib: true do + describe '#find' do + let(:project) { create(:project, :public, :repository) } + let(:finder) { described_class.new(project, project.default_branch) } + + it 'finds by name' do + results = finder.find('files') + expect(results.map(&:first)).to include('files/images/wm.svg') + end + + it 'finds by content' do + results = finder.find('files') + + blob = results.select { |result| result.first == "CHANGELOG" }.flatten.last + + expect(blob.filename).to eq("CHANGELOG") + end + end +end diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index 6e0b1192706..1b8690ba613 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -55,7 +55,7 @@ describe Gitlab::ProjectSearchResults, lib: true do end it 'finds by name' do - expect(results).to include(["files/images/wm.svg", nil]) + expect(results.map(&:first)).to include('files/images/wm.svg') end it 'finds by content' do -- cgit v1.2.1 From 13207310b5bc7439aeaf549febb1f9c8a55f4778 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Fri, 12 May 2017 10:22:50 +0000 Subject: Fix accessibility issues for Input fields across GitLab --- app/helpers/button_helper.rb | 5 ++++- app/views/layouts/_search.html.haml | 2 +- app/views/projects/edit.html.haml | 4 ++-- app/views/projects/new.html.haml | 2 +- app/views/shared/_clone_panel.html.haml | 2 +- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index c85e96cf78d..206d0753f08 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -42,7 +42,10 @@ module ButtonHelper class: "btn #{css_class}", data: data, type: :button, - title: title + title: title, + aria: { + label: title + } end def http_clone_button(project, placement = 'right', append_link: true) diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index 0e64ebd71b8..b689991bb6d 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -13,7 +13,7 @@ .location-badge= label .search-input-wrap .dropdown{ data: { url: search_autocomplete_path } } - = search_field_tag 'search', nil, placeholder: 'Search', class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options', spellcheck: false, tabindex: '1', autocomplete: 'off', data: { toggle: 'dropdown', issues_path: issues_dashboard_url, mr_path: merge_requests_dashboard_url } + = search_field_tag 'search', nil, placeholder: 'Search', class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options', spellcheck: false, tabindex: '1', autocomplete: 'off', data: { toggle: 'dropdown', issues_path: issues_dashboard_url, mr_path: merge_requests_dashboard_url }, aria: { label: 'Search' } .dropdown-menu.dropdown-select = dropdown_content do %ul diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 160345cfaa5..d9643dc7957 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -40,8 +40,8 @@ .form_group.prepend-top-20.sharing-and-permissions .row.js-visibility-select .col-md-9 - %label.label-light - = label_tag :project_visibility, 'Project Visibility', class: 'label-light' + .label-light + = label_tag :project_visibility, 'Project Visibility', class: 'label-light', for: :project_visibility_level = link_to "(?)", help_page_path("public_access/public_access") %span.help-block .col-md-3.visibility-select-container diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 9e292729425..e180cb8bad1 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -30,7 +30,7 @@ #{root_url}#{current_user.username}/ = f.hidden_field :namespace_id, value: current_user.namespace_id .form-group.col-xs-12.col-sm-6.project-path - = f.label :namespace_id, class: 'label-light' do + = f.label :path, class: 'label-light' do %span Project name = f.text_field :path, placeholder: "my-awesome-project", class: "form-control", tabindex: 2, autofocus: true, required: true diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index 34a4d7398bc..0992a65f7cd 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -17,7 +17,7 @@ %li = http_clone_button(project) - = text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true + = text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true, aria: { label: 'Project clone URL' } .input-group-btn = clipboard_button(target: '#project_clone', title: "Copy URL to clipboard") -- cgit v1.2.1 From 4dd2195004c63a63c67dfadb55097af1857df7b9 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 12 May 2017 11:41:39 +0100 Subject: Style changes to Ruby file --- app/models/concerns/protected_branch_access.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/models/concerns/protected_branch_access.rb b/app/models/concerns/protected_branch_access.rb index 565d6a48192..a40148a4394 100644 --- a/app/models/concerns/protected_branch_access.rb +++ b/app/models/concerns/protected_branch_access.rb @@ -8,9 +8,13 @@ module ProtectedBranchAccess delegate :project, to: :protected_branch - validates :access_level, presence: true, inclusion: { in: [Gitlab::Access::MASTER, - Gitlab::Access::DEVELOPER, - Gitlab::Access::NO_ACCESS] } + validates :access_level, presence: true, inclusion: { + in: [ + Gitlab::Access::MASTER, + Gitlab::Access::DEVELOPER, + Gitlab::Access::NO_ACCESS + ] + } def self.human_access_levels { -- cgit v1.2.1 From e2a3a5095abd8c1fa57dd19e4ff693ae4021fde8 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Wed, 10 May 2017 23:54:10 +0300 Subject: Move update_assignee_cache_counts to the service --- app/models/issue_assignee.rb | 7 ---- app/models/merge_request.rb | 8 ----- app/models/user.rb | 5 +++ app/services/issuable_base_service.rb | 6 ++++ app/services/members/authorized_destroy_service.rb | 3 +- spec/features/dashboard/issuables_counter_spec.rb | 4 ++- spec/models/issue_spec.rb | 40 --------------------- spec/models/merge_request_spec.rb | 42 ---------------------- spec/services/issues/create_service_spec.rb | 16 +++++++++ spec/services/issues/update_service_spec.rb | 7 ++++ .../services/merge_requests/create_service_spec.rb | 20 +++++++++++ .../services/merge_requests/update_service_spec.rb | 9 +++++ 12 files changed, 68 insertions(+), 99 deletions(-) diff --git a/app/models/issue_assignee.rb b/app/models/issue_assignee.rb index 0663d3aaef8..06d760b6a89 100644 --- a/app/models/issue_assignee.rb +++ b/app/models/issue_assignee.rb @@ -3,11 +3,4 @@ class IssueAssignee < ActiveRecord::Base belongs_to :issue belongs_to :assignee, class_name: "User", foreign_key: :user_id - - after_create :update_assignee_cache_counts - after_destroy :update_assignee_cache_counts - - def update_assignee_cache_counts - assignee&.update_cache_counts - end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 59736f70f24..9e5db53b15e 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -125,7 +125,6 @@ class MergeRequest < ActiveRecord::Base participant :assignee after_save :keep_around_commit - after_save :update_assignee_cache_counts, if: :assignee_id_changed? def self.reference_prefix '!' @@ -187,13 +186,6 @@ class MergeRequest < ActiveRecord::Base work_in_progress?(title) ? title : "WIP: #{title}" end - def update_assignee_cache_counts - # make sure we flush the cache for both the old *and* new assignees(if they exist) - previous_assignee = User.find_by_id(assignee_id_was) if assignee_id_was - previous_assignee&.update_cache_counts - assignee&.update_cache_counts - end - # Returns a Hash of attributes to be used for Twitter card metadata def card_attributes { diff --git a/app/models/user.rb b/app/models/user.rb index f713a20233c..c7160a6af14 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -929,6 +929,11 @@ class User < ActiveRecord::Base assigned_open_issues_count(force: true) end + def invalidate_cache_counts + Rails.cache.delete(['users', id, 'assigned_open_merge_requests_count']) + Rails.cache.delete(['users', id, 'assigned_open_issues_count']) + end + def todos_done_count(force: false) Rails.cache.fetch(['users', id, 'todos_done_count'], force: force) do TodosFinder.new(self, state: :done).execute.count diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index c1e532b504a..dc2ab99b982 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -178,6 +178,7 @@ class IssuableBaseService < BaseService after_create(issuable) issuable.create_cross_references!(current_user) execute_hooks(issuable) + issuable.assignees.each(&:invalidate_cache_counts) end issuable @@ -234,6 +235,11 @@ class IssuableBaseService < BaseService old_assignees: old_assignees ) + if old_assignees != issuable.assignees + assignees = old_assignees + issuable.assignees.to_a + assignees.compact.each(&:invalidate_cache_counts) + end + after_update(issuable) issuable.create_new_cross_references!(current_user) execute_hooks(issuable, 'update') diff --git a/app/services/members/authorized_destroy_service.rb b/app/services/members/authorized_destroy_service.rb index a85b9465c84..7912cac65d3 100644 --- a/app/services/members/authorized_destroy_service.rb +++ b/app/services/members/authorized_destroy_service.rb @@ -43,8 +43,9 @@ module Members ) project.merge_requests.opened.assigned_to(member.user).update_all(assignee_id: nil) - member.user.update_cache_counts end + + member.user.invalidate_cache_counts end end end diff --git a/spec/features/dashboard/issuables_counter_spec.rb b/spec/features/dashboard/issuables_counter_spec.rb index 6f7bf0eba6e..354267dbee7 100644 --- a/spec/features/dashboard/issuables_counter_spec.rb +++ b/spec/features/dashboard/issuables_counter_spec.rb @@ -19,7 +19,7 @@ describe 'Navigation bar counter', feature: true, caching: true do issue.assignees = [] - user.update_cache_counts + user.invalidate_cache_counts Timecop.travel(3.minutes.from_now) do visit issues_path @@ -35,6 +35,8 @@ describe 'Navigation bar counter', feature: true, caching: true do merge_request.update(assignee: nil) + user.invalidate_cache_counts + Timecop.travel(3.minutes.from_now) do visit merge_requests_path diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 725f5c2311f..bb4e70db2e9 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -38,46 +38,6 @@ describe Issue, models: true do end end - describe "before_save" do - describe "#update_cache_counts when an issue is reassigned" do - let(:issue) { create(:issue) } - let(:assignee) { create(:user) } - - context "when previous assignee exists" do - before do - issue.project.team << [assignee, :developer] - issue.assignees << assignee - end - - it "updates cache counts for new assignee" do - user = create(:user) - - expect(user).to receive(:update_cache_counts) - - issue.assignees << user - end - - it "updates cache counts for previous assignee" do - issue.assignees.first - - expect_any_instance_of(User).to receive(:update_cache_counts) - - issue.assignees.destroy_all - end - end - - context "when previous assignee does not exist" do - it "updates cache count for the new assignee" do - issue.assignees = [] - - expect_any_instance_of(User).to receive(:update_cache_counts) - - issue.assignees << assignee - end - end - end - end - describe '#card_attributes' do it 'includes the author name' do allow(subject).to receive(:author).and_return(double(name: 'Robert')) diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index ef349530761..e8124c4cbe9 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -87,48 +87,6 @@ describe MergeRequest, models: true do end end - describe "before_save" do - describe "#update_cache_counts when a merge request is reassigned" do - let(:project) { create :project } - let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } - let(:assignee) { create :user } - - context "when previous assignee exists" do - before do - project.team << [assignee, :developer] - merge_request.update(assignee: assignee) - end - - it "updates cache counts for new assignee" do - user = create(:user) - - expect(user).to receive(:update_cache_counts) - - merge_request.update(assignee: user) - end - - it "updates cache counts for previous assignee" do - old_assignee = merge_request.assignee - allow(User).to receive(:find_by_id).with(old_assignee.id).and_return(old_assignee) - - expect(old_assignee).to receive(:update_cache_counts) - - merge_request.update(assignee: nil) - end - end - - context "when previous assignee does not exist" do - it "updates cache count for the new assignee" do - merge_request.update(assignee: nil) - - expect_any_instance_of(User).to receive(:update_cache_counts) - - merge_request.update(assignee: assignee) - end - end - end - end - describe '#card_attributes' do it 'includes the author name' do allow(subject).to receive(:author).and_return(double(name: 'Robert')) diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index 01edc46496d..dab1a3469f7 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -118,6 +118,22 @@ describe Issues::CreateService, services: true do end end + context 'when assignee is set' do + let(:opts) do + { title: 'Title', + description: 'Description', + assignees: [assignee] } + end + + it 'invalidates open issues counter for assignees when issue is assigned' do + project.team << [assignee, :master] + + described_class.new(project, user, opts).execute + + expect(assignee.assigned_open_issues_count).to eq 1 + end + end + it 'executes issue hooks when issue is not confidential' do opts = { title: 'Title', description: 'Description', confidential: false } diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 1954d8739f6..5184c1d5f19 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -59,6 +59,13 @@ describe Issues::UpdateService, services: true do expect(issue.due_date).to eq Date.tomorrow end + it 'updates open issue counter for assignees when issue is reassigned' do + update_issue(assignee_ids: [user2.id]) + + expect(user3.assigned_open_issues_count).to eq 0 + expect(user2.assigned_open_issues_count).to eq 1 + end + it 'sorts issues as specified by parameters' do issue1 = create(:issue, project: project, assignees: [user3]) issue2 = create(:issue, project: project, assignees: [user3]) diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index ace82380cc9..41752f1a01a 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -144,6 +144,26 @@ describe MergeRequests::CreateService, services: true do expect(merge_request.assignee).to eq(assignee) end + context 'when assignee is set' do + let(:opts) do + { + title: 'Title', + description: 'Description', + assignee_id: assignee.id, + source_branch: 'feature', + target_branch: 'master' + } + end + + it 'invalidates open merge request counter for assignees when merge request is assigned' do + project.team << [assignee, :master] + + described_class.new(project, user, opts).execute + + expect(assignee.assigned_open_merge_requests_count).to eq 1 + end + end + context "when issuable feature is private" do before do project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE, diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 07f5440cc36..2c8fbb46e75 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -299,6 +299,15 @@ describe MergeRequests::UpdateService, services: true do end end + context 'when the assignee changes' do + it 'updates open merge request counter for assignees when merge request is reassigned' do + update_merge_request(assignee_id: user2.id) + + expect(user3.assigned_open_merge_requests_count).to eq 0 + expect(user2.assigned_open_merge_requests_count).to eq 1 + end + end + context 'when the target branch change' do before do update_merge_request({ target_branch: 'target' }) -- cgit v1.2.1 From 4c9264b695c2d44cc1c43febc88fc167786a4b52 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Fri, 12 May 2017 11:18:42 +0000 Subject: Fix milestone_filter_spec.rb by using wait_for_ajax and a correct user flow --- spec/features/dashboard/milestone_filter_spec.rb | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/spec/features/dashboard/milestone_filter_spec.rb b/spec/features/dashboard/milestone_filter_spec.rb index e9ca7fd50e7..d60a002a8d7 100644 --- a/spec/features/dashboard/milestone_filter_spec.rb +++ b/spec/features/dashboard/milestone_filter_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' -describe 'Dashboard > milestone filter', feature: true, js: true do +describe 'Dashboard > milestone filter', :feature, :js do + include WaitForAjax + let(:user) { create(:user) } let(:project) { create(:project, name: 'test', namespace: user.namespace) } let(:milestone) { create(:milestone, title: "v1.0", project: project) } @@ -26,30 +28,29 @@ describe 'Dashboard > milestone filter', feature: true, js: true do before do find(milestone_select).click + wait_for_ajax page.within('.dropdown-content') do click_link 'v1.0' end find(milestone_select).click + wait_for_ajax end it 'shows issues with Milestone v1.0' do expect(find('.issues-list')).to have_selector('.issue', count: 1) - - find(milestone_select).click - expect(find('.dropdown-content')).to have_selector('a.is-active', count: 1) end it 'should not change active Milestone unless clicked' do - find(milestone_select).trigger('click') - expect(find('.dropdown-content')).to have_selector('a.is-active', count: 1) # open & close dropdown find('.dropdown-menu-close').click + expect(find('.milestone-filter')).not_to have_selector('.dropdown.open') + find(milestone_select).click expect(find('.dropdown-content')).to have_selector('a.is-active', count: 1) -- cgit v1.2.1 From a1348a5d2932784b29b0b9fb18e519f18c085e62 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 12 May 2017 11:28:26 +0000 Subject: Uniform CI status components in vue --- .../javascripts/pipelines/components/stage.js | 104 --------------------- .../javascripts/pipelines/components/status.js | 60 ------------ .../components/mr_widget_deployment.js | 4 +- .../components/mr_widget_pipeline.js | 21 +++-- .../javascripts/vue_shared/ci_status_icons.js | 12 --- .../vue_shared/components/ci_badge_link.vue | 52 +++++++++++ .../javascripts/vue_shared/components/ci_icon.vue | 25 ++++- .../vue_shared/components/pipeline_status_icon.js | 23 ----- .../vue_shared/components/pipelines_table_row.js | 15 ++- .../javascripts/vue_shared/pipeline_svg_icons.js | 43 --------- app/assets/stylesheets/pages/merge_requests.scss | 8 +- changelogs/unreleased/30286-ci-badge-component.yml | 4 + .../components/mr_widget_deployment_spec.js | 4 +- .../components/mr_widget_pipeline_spec.js | 6 +- .../vue_shared/components/ci_badge_link_spec.js | 89 ++++++++++++++++++ .../vue_shared/components/ci_icon_spec.js | 9 ++ 16 files changed, 213 insertions(+), 266 deletions(-) delete mode 100644 app/assets/javascripts/pipelines/components/stage.js delete mode 100644 app/assets/javascripts/pipelines/components/status.js create mode 100644 app/assets/javascripts/vue_shared/components/ci_badge_link.vue delete mode 100644 app/assets/javascripts/vue_shared/components/pipeline_status_icon.js delete mode 100644 app/assets/javascripts/vue_shared/pipeline_svg_icons.js create mode 100644 changelogs/unreleased/30286-ci-badge-component.yml create mode 100644 spec/javascripts/vue_shared/components/ci_badge_link_spec.js diff --git a/app/assets/javascripts/pipelines/components/stage.js b/app/assets/javascripts/pipelines/components/stage.js deleted file mode 100644 index 034e8d3280e..00000000000 --- a/app/assets/javascripts/pipelines/components/stage.js +++ /dev/null @@ -1,104 +0,0 @@ -/* global Flash */ -import { borderlessStatusIconEntityMap } from '../../vue_shared/ci_status_icons'; - -export default { - data() { - return { - builds: '', - spinner: '', - }; - }, - - props: { - stage: { - type: Object, - required: true, - }, - }, - - updated() { - if (this.builds) { - this.stopDropdownClickPropagation(); - } - }, - - methods: { - fetchBuilds(e) { - const ariaExpanded = e.currentTarget.attributes['aria-expanded']; - - if (ariaExpanded && (ariaExpanded.textContent === 'true')) return null; - - return this.$http.get(this.stage.dropdown_path) - .then((response) => { - this.builds = JSON.parse(response.body).html; - }, () => { - const flash = new Flash('Something went wrong on our end.'); - return flash; - }); - }, - - /** - * When the user right clicks or cmd/ctrl + click in the job name - * the dropdown should not be closed and the link should open in another tab, - * so we stop propagation of the click event inside the dropdown. - * - * Since this component is rendered multiple times per page we need to guarantee we only - * target the click event of this component. - */ - stopDropdownClickPropagation() { - $(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')).on('click', (e) => { - e.stopPropagation(); - }); - }, - }, - computed: { - buildsOrSpinner() { - return this.builds ? this.builds : this.spinner; - }, - dropdownClass() { - if (this.builds) return 'js-builds-dropdown-container'; - return 'js-builds-dropdown-loading builds-dropdown-loading'; - }, - buildStatus() { - return `Build: ${this.stage.status.label}`; - }, - tooltip() { - return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`; - }, - triggerButtonClass() { - return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`; - }, - svgHTML() { - return borderlessStatusIconEntityMap[this.stage.status.icon]; - }, - }, - watch: { - 'stage.title': function stageTitle() { - $(this.$refs.button).tooltip('destroy').tooltip(); - }, - }, - template: ` -
    - - -
    - `, -}; diff --git a/app/assets/javascripts/pipelines/components/status.js b/app/assets/javascripts/pipelines/components/status.js deleted file mode 100644 index 21a281af438..00000000000 --- a/app/assets/javascripts/pipelines/components/status.js +++ /dev/null @@ -1,60 +0,0 @@ -import canceledSvg from 'icons/_icon_status_canceled.svg'; -import createdSvg from 'icons/_icon_status_created.svg'; -import failedSvg from 'icons/_icon_status_failed.svg'; -import manualSvg from 'icons/_icon_status_manual.svg'; -import pendingSvg from 'icons/_icon_status_pending.svg'; -import runningSvg from 'icons/_icon_status_running.svg'; -import skippedSvg from 'icons/_icon_status_skipped.svg'; -import successSvg from 'icons/_icon_status_success.svg'; -import warningSvg from 'icons/_icon_status_warning.svg'; - -export default { - props: { - pipeline: { - type: Object, - required: true, - }, - }, - - data() { - const svgsDictionary = { - icon_status_canceled: canceledSvg, - icon_status_created: createdSvg, - icon_status_failed: failedSvg, - icon_status_manual: manualSvg, - icon_status_pending: pendingSvg, - icon_status_running: runningSvg, - icon_status_skipped: skippedSvg, - icon_status_success: successSvg, - icon_status_warning: warningSvg, - }; - - return { - svg: svgsDictionary[this.pipeline.details.status.icon], - }; - }, - - computed: { - cssClasses() { - return `ci-status ci-${this.pipeline.details.status.group}`; - }, - - detailsPath() { - const { status } = this.pipeline.details; - return status.has_details ? status.details_path : false; - }, - - content() { - return `${this.svg} ${this.pipeline.details.status.text}`; - }, - }, - template: ` - - - - - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js index 3c23b8e472b..8b59e018836 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js @@ -1,7 +1,7 @@ /* global Flash */ import '~/lib/utils/datetime_utility'; -import { statusClassToSvgMap } from '../../vue_shared/pipeline_svg_icons'; +import { statusIconEntityMap } from '../../vue_shared/ci_status_icons'; import MemoryUsage from './mr_widget_memory_usage'; import MRWidgetService from '../services/mr_widget_service'; @@ -16,7 +16,7 @@ export default { }, computed: { svg() { - return statusClassToSvgMap.icon_status_success; + return statusIconEntityMap.icon_status_success; }, }, methods: { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js index 801b9fb1ba1..d8c27013cd3 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js @@ -1,6 +1,6 @@ -import PipelineStage from '../../pipelines/components/stage'; -import pipelineStatusIcon from '../../vue_shared/components/pipeline_status_icon'; -import { statusClassToSvgMap } from '../../vue_shared/pipeline_svg_icons'; +import PipelineStage from '../../pipelines/components/stage.vue'; +import ciIcon from '../../vue_shared/components/ci_icon.vue'; +import { statusIconEntityMap } from '../../vue_shared/ci_status_icons'; export default { name: 'MRWidgetPipeline', @@ -9,7 +9,7 @@ export default { }, components: { 'pipeline-stage': PipelineStage, - 'pipeline-status-icon': pipelineStatusIcon, + ciIcon, }, computed: { hasCIError() { @@ -18,11 +18,14 @@ export default { return hasCI && !ciStatus; }, svg() { - return statusClassToSvgMap.icon_status_failed; + return statusIconEntityMap.icon_status_failed; }, stageText() { return this.mr.pipeline.details.stages.length > 1 ? 'stages' : 'stage'; }, + status() { + return this.mr.pipeline.details.status || {}; + }, }, template: `
    @@ -38,7 +41,13 @@ export default { Could not connect to the CI server. Please check your settings and try again.