From 0532bff6d41fd3c685c88622f34fa726f171568a Mon Sep 17 00:00:00 2001 From: Bryce Johnson Date: Mon, 7 Aug 2017 20:55:50 +0000 Subject: Group-level new issue & MR using previously selected project --- app/assets/javascripts/project_select.js | 17 ++-- .../javascripts/project_select_combo_button.js | 85 +++++++++++++++++ app/assets/stylesheets/framework/nav.scss | 26 ++++- .../shared/_new_project_item_select.html.haml | 7 +- changelogs/unreleased/group-new-issue.yml | 4 + spec/features/dashboard/issues_spec.rb | 15 ++- spec/features/groups/empty_states_spec.rb | 4 +- .../fixtures/project_select_combo_button.html.haml | 6 ++ .../project_select_combo_button_spec.js | 105 +++++++++++++++++++++ 9 files changed, 251 insertions(+), 18 deletions(-) create mode 100644 app/assets/javascripts/project_select_combo_button.js create mode 100644 changelogs/unreleased/group-new-issue.yml create mode 100644 spec/javascripts/fixtures/project_select_combo_button.html.haml create mode 100644 spec/javascripts/project_select_combo_button_spec.js diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index ebcefc819f5..1b4ed6be90a 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -1,5 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-var, comma-dangle, object-shorthand, one-var, one-var-declaration-per-line, no-else-return, quotes, max-len */ import Api from './api'; +import ProjectSelectComboButton from './project_select_combo_button'; (function() { this.ProjectSelect = (function() { @@ -58,7 +59,8 @@ import Api from './api'; if (this.includeGroups) { placeholder += " or group"; } - return $(select).select2({ + + $(select).select2({ placeholder: placeholder, minimumInputLength: 0, query: (function(_this) { @@ -96,21 +98,18 @@ import Api from './api'; }; })(this), id: function(project) { - return project.web_url; + return JSON.stringify({ + name: project.name, + url: project.web_url, + }); }, text: function(project) { return project.name_with_namespace || project.name; }, dropdownCssClass: "ajax-project-dropdown" }); - }); - - $('.new-project-item-select-button').on('click', function() { - $('.project-item-select', this.parentNode).select2('open'); - }); - $('.project-item-select').on('click', function() { - window.location = `${$(this).val()}/${this.dataset.relativePath}`; + return new ProjectSelectComboButton(select); }); } diff --git a/app/assets/javascripts/project_select_combo_button.js b/app/assets/javascripts/project_select_combo_button.js new file mode 100644 index 00000000000..f799d9d619a --- /dev/null +++ b/app/assets/javascripts/project_select_combo_button.js @@ -0,0 +1,85 @@ +import AccessorUtilities from './lib/utils/accessor'; + +export default class ProjectSelectComboButton { + constructor(select) { + this.projectSelectInput = $(select); + this.newItemBtn = $('.new-project-item-link'); + this.newItemBtnBaseText = this.newItemBtn.data('label'); + this.itemType = this.deriveItemTypeFromLabel(); + this.groupId = this.projectSelectInput.data('groupId'); + + this.bindEvents(); + this.initLocalStorage(); + } + + bindEvents() { + this.projectSelectInput.siblings('.new-project-item-select-button') + .on('click', this.openDropdown); + + this.projectSelectInput.on('change', () => this.selectProject()); + } + + initLocalStorage() { + const localStorageIsSafe = AccessorUtilities.isLocalStorageAccessSafe(); + + if (localStorageIsSafe) { + const itemTypeKebabed = this.newItemBtnBaseText.toLowerCase().split(' ').join('-'); + + this.localStorageKey = ['group', this.groupId, itemTypeKebabed, 'recent-project'].join('-'); + this.setBtnTextFromLocalStorage(); + } + } + + openDropdown() { + $(this).siblings('.project-item-select').select2('open'); + } + + selectProject() { + const selectedProjectData = JSON.parse(this.projectSelectInput.val()); + const projectUrl = `${selectedProjectData.url}/${this.projectSelectInput.data('relativePath')}`; + const projectName = selectedProjectData.name; + + const projectMeta = { + url: projectUrl, + name: projectName, + }; + + this.setNewItemBtnAttributes(projectMeta); + this.setProjectInLocalStorage(projectMeta); + } + + setBtnTextFromLocalStorage() { + const cachedProjectData = this.getProjectFromLocalStorage(); + + this.setNewItemBtnAttributes(cachedProjectData); + } + + setNewItemBtnAttributes(project) { + if (project) { + this.newItemBtn.attr('href', project.url); + this.newItemBtn.text(`${this.newItemBtnBaseText} in ${project.name}`); + this.newItemBtn.enable(); + } else { + this.newItemBtn.text(`Select project to create ${this.itemType}`); + this.newItemBtn.disable(); + } + } + + deriveItemTypeFromLabel() { + // label is either 'New issue' or 'New merge request' + return this.newItemBtnBaseText.split(' ').slice(1).join(' '); + } + + getProjectFromLocalStorage() { + const projectString = localStorage.getItem(this.localStorageKey); + + return JSON.parse(projectString); + } + + setProjectInLocalStorage(projectMeta) { + const projectString = JSON.stringify(projectMeta); + + localStorage.setItem(this.localStorageKey, projectString); + } +} + diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 88e7ba117d5..d386ac5ba9c 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -251,7 +251,6 @@ // Applies on /dashboard/issues .project-item-select-holder { - display: block; margin: 0; } } @@ -283,6 +282,31 @@ } } +.project-item-select-holder.btn-group { + display: flex; + max-width: 350px; + overflow: hidden; + + @media(max-width: $screen-xs-max) { + width: 100%; + max-width: none; + } + + .new-project-item-link { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .new-project-item-select-button { + width: 32px; + } +} + +.new-project-item-select-button .fa-caret-down { + margin-left: 2px; +} + .layout-nav { width: 100%; background: $gray-light; diff --git a/app/views/shared/_new_project_item_select.html.haml b/app/views/shared/_new_project_item_select.html.haml index b417e83cdb6..96502d7ce93 100644 --- a/app/views/shared/_new_project_item_select.html.haml +++ b/app/views/shared/_new_project_item_select.html.haml @@ -1,6 +1,7 @@ - if any_projects?(@projects) - .project-item-select-holder + .project-item-select-holder.btn-group.pull-right + %a.btn.btn-new.new-project-item-link{ href: '', data: { label: local_assigns[:label] } } + = icon('spinner spin') = project_select_tag :project_path, class: "project-item-select", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at', relative_path: local_assigns[:path] }, with_feature_enabled: local_assigns[:with_feature_enabled] - %a.btn.btn-new.new-project-item-select-button - = local_assigns[:label] + %button.btn.btn-new.new-project-item-select-button = icon('caret-down') diff --git a/changelogs/unreleased/group-new-issue.yml b/changelogs/unreleased/group-new-issue.yml new file mode 100644 index 00000000000..5480a44526b --- /dev/null +++ b/changelogs/unreleased/group-new-issue.yml @@ -0,0 +1,4 @@ +--- +title: Cache recent projects for group-level new resource creation. +merge_request: !13058 +author: diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb index be6f78ee607..795335aa106 100644 --- a/spec/features/dashboard/issues_spec.rb +++ b/spec/features/dashboard/issues_spec.rb @@ -79,12 +79,21 @@ RSpec.describe 'Dashboard Issues' do end end - it 'shows the new issue page', :js do + it 'shows the new issue page', js: true do find('.new-project-item-select-button').trigger('click') + wait_for_requests - find('.select2-results li').click - expect(page).to have_current_path("/#{project.path_with_namespace}/issues/new") + project_path = "/#{project.path_with_namespace}" + project_json = { name: project.name_with_namespace, url: project_path }.to_json + + # similate selection, and prevent overlap by dropdown menu + execute_script("$('.project-item-select').val('#{project_json}').trigger('change');") + execute_script("$('#select2-drop-mask').remove();") + + find('.new-project-item-link').trigger('click') + + expect(page).to have_current_path("#{project_path}/issues/new") page.within('#content-body') do expect(page).to have_selector('.issue-form') diff --git a/spec/features/groups/empty_states_spec.rb b/spec/features/groups/empty_states_spec.rb index 7f28553c44e..243e8536168 100644 --- a/spec/features/groups/empty_states_spec.rb +++ b/spec/features/groups/empty_states_spec.rb @@ -38,7 +38,7 @@ feature 'Groups Merge Requests Empty States' do it 'should show a new merge request button' do within '.empty-state' do - expect(page).to have_content('New merge request') + expect(page).to have_content('create merge request') end end @@ -63,7 +63,7 @@ feature 'Groups Merge Requests Empty States' do it 'should not show a new merge request button' do within '.empty-state' do - expect(page).not_to have_link('New merge request') + expect(page).not_to have_link('create merge request') end end end diff --git a/spec/javascripts/fixtures/project_select_combo_button.html.haml b/spec/javascripts/fixtures/project_select_combo_button.html.haml new file mode 100644 index 00000000000..54bc1a59279 --- /dev/null +++ b/spec/javascripts/fixtures/project_select_combo_button.html.haml @@ -0,0 +1,6 @@ +.project-item-select-holder + %input.project-item-select{ data: { group_id: '12345' , relative_path: 'issues/new' } } + %a.new-project-item-link{ data: { label: 'New issue' }, href: ''} + %i.fa.fa-spinner.spin + %a.new-project-item-select-button + %i.fa.fa-caret-down diff --git a/spec/javascripts/project_select_combo_button_spec.js b/spec/javascripts/project_select_combo_button_spec.js new file mode 100644 index 00000000000..e10a5a3bef6 --- /dev/null +++ b/spec/javascripts/project_select_combo_button_spec.js @@ -0,0 +1,105 @@ +import ProjectSelectComboButton from '~/project_select_combo_button'; + +const fixturePath = 'static/project_select_combo_button.html.raw'; + +describe('Project Select Combo Button', function () { + preloadFixtures(fixturePath); + + beforeEach(function () { + this.defaults = { + label: 'Select project to create issue', + groupId: 12345, + projectMeta: { + name: 'My Cool Project', + url: 'http://mycoolproject.com', + }, + newProjectMeta: { + name: 'My Other Cool Project', + url: 'http://myothercoolproject.com', + }, + localStorageKey: 'group-12345-new-issue-recent-project', + relativePath: 'issues/new', + }; + + loadFixtures(fixturePath); + + this.newItemBtn = document.querySelector('.new-project-item-link'); + this.projectSelectInput = document.querySelector('.project-item-select'); + }); + + describe('on page load when localStorage is empty', function () { + beforeEach(function () { + this.comboButton = new ProjectSelectComboButton(this.projectSelectInput); + }); + + it('newItemBtn is disabled', function () { + expect(this.newItemBtn.hasAttribute('disabled')).toBe(true); + expect(this.newItemBtn.classList.contains('disabled')).toBe(true); + }); + + it('newItemBtn href is null', function () { + expect(this.newItemBtn.getAttribute('href')).toBe(''); + }); + + it('newItemBtn text is the plain default label', function () { + expect(this.newItemBtn.textContent).toBe(this.defaults.label); + }); + }); + + describe('on page load when localStorage is filled', function () { + beforeEach(function () { + window.localStorage + .setItem(this.defaults.localStorageKey, JSON.stringify(this.defaults.projectMeta)); + this.comboButton = new ProjectSelectComboButton(this.projectSelectInput); + }); + + it('newItemBtn is not disabled', function () { + expect(this.newItemBtn.hasAttribute('disabled')).toBe(false); + expect(this.newItemBtn.classList.contains('disabled')).toBe(false); + }); + + it('newItemBtn href is correctly set', function () { + expect(this.newItemBtn.getAttribute('href')).toBe(this.defaults.projectMeta.url); + }); + + it('newItemBtn text is the cached label', function () { + expect(this.newItemBtn.textContent) + .toBe(`New issue in ${this.defaults.projectMeta.name}`); + }); + + afterEach(function () { + window.localStorage.clear(); + }); + }); + + describe('after selecting a new project', function () { + beforeEach(function () { + this.comboButton = new ProjectSelectComboButton(this.projectSelectInput); + + // mock the effect of selecting an item from the projects dropdown (select2) + $('.project-item-select') + .val(JSON.stringify(this.defaults.newProjectMeta)) + .trigger('change'); + }); + + it('newItemBtn is not disabled', function () { + expect(this.newItemBtn.hasAttribute('disabled')).toBe(false); + expect(this.newItemBtn.classList.contains('disabled')).toBe(false); + }); + + it('newItemBtn href is correctly set', function () { + expect(this.newItemBtn.getAttribute('href')) + .toBe('http://myothercoolproject.com/issues/new'); + }); + + it('newItemBtn text is the selected project label', function () { + expect(this.newItemBtn.textContent) + .toBe(`New issue in ${this.defaults.newProjectMeta.name}`); + }); + + afterEach(function () { + window.localStorage.clear(); + }); + }); +}); + -- cgit v1.2.1