From 63670bb7f84d1c728c32a1914ed378de4e4ac0de Mon Sep 17 00:00:00 2001 From: keen99 Date: Tue, 29 Mar 2016 21:51:31 +0000 Subject: existing folders and existing repos should not use the same set of instructions - pushing an existing repo into gitlab using the previous instructions would result in loss (at the gitlab remote end) of exist branches and tags. so lets push those! --- app/views/projects/empty.html.haml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 6ad7b05155a..05e73326ee4 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -44,7 +44,7 @@ git push -u origin master %fieldset - %h5 Existing folder or Git repository + %h5 Existing folder %pre.light-well :preserve cd existing_folder @@ -54,6 +54,15 @@ git commit git push -u origin master + %fieldset + %h5 Existing Git repository + %pre.light-well + :preserve + cd existing_repo + git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} + git push -u origin --all + git push -u origin --tags + - if can? current_user, :remove_project, @project .prepend-top-20 = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right" -- cgit v1.2.1 From 4e90f1009d3b587a7d56936f4e960d222d7f7993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Sun, 3 Apr 2016 13:44:32 +0000 Subject: use plain shell no bashism detected here --- scripts/notify_slack.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/notify_slack.sh b/scripts/notify_slack.sh index 0a4239e132c..6b3bc563c7a 100755 --- a/scripts/notify_slack.sh +++ b/scripts/notify_slack.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/sh # Sends Slack notification ERROR_MSG to CHANNEL # An env. variable CI_SLACK_WEBHOOK_URL needs to be set. -- cgit v1.2.1 From 5bf01bb7b993250ad24ebcc313eea1cf32c6908b Mon Sep 17 00:00:00 2001 From: Lee Matos Date: Tue, 13 Dec 2016 20:05:46 +0000 Subject: Clarify instructions for importing Wiki/Merge Request data --- doc/workflow/importing/import_projects_from_gitlab_com.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/workflow/importing/import_projects_from_gitlab_com.md b/doc/workflow/importing/import_projects_from_gitlab_com.md index dcc00074b75..ae4e92bd7cd 100644 --- a/doc/workflow/importing/import_projects_from_gitlab_com.md +++ b/doc/workflow/importing/import_projects_from_gitlab_com.md @@ -5,6 +5,8 @@ GitLab support is enabled on your GitLab instance. You can read more about GitLab support [here](http://docs.gitlab.com/ce/integration/gitlab.html) To get to the importer page you need to go to "New project" page. +Note: If you are interested in importing Wiki and Merge Request data to your new instance, you'll need to follow the instructions for [project export](http://docs.gitlab.com/ce/user/project/settings/import_export.html) + ![New project page](gitlab_importer/new_project_page.png) Click on the "Import projects from GitLab.com" link and you will be redirected to GitLab.com -- cgit v1.2.1 From 3a6cbd669e657c198ca7fb5023def9a92e38f7ba Mon Sep 17 00:00:00 2001 From: Lee Matos Date: Thu, 15 Dec 2016 14:29:54 +0000 Subject: applying style guide fixes --- doc/workflow/importing/import_projects_from_gitlab_com.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/workflow/importing/import_projects_from_gitlab_com.md b/doc/workflow/importing/import_projects_from_gitlab_com.md index ae4e92bd7cd..bbb39e1641c 100644 --- a/doc/workflow/importing/import_projects_from_gitlab_com.md +++ b/doc/workflow/importing/import_projects_from_gitlab_com.md @@ -5,7 +5,8 @@ GitLab support is enabled on your GitLab instance. You can read more about GitLab support [here](http://docs.gitlab.com/ce/integration/gitlab.html) To get to the importer page you need to go to "New project" page. -Note: If you are interested in importing Wiki and Merge Request data to your new instance, you'll need to follow the instructions for [project export](http://docs.gitlab.com/ce/user/project/settings/import_export.html) +>**Note:** +If you are interested in importing Wiki and Merge Request data to your new instance, you'll need to follow the instructions for [project export](../../user/project/settings/import_export.html) ![New project page](gitlab_importer/new_project_page.png) -- cgit v1.2.1 From 24ef4081a6723c22d54cf3492efaaad9fba1f77f Mon Sep 17 00:00:00 2001 From: Lee Matos Date: Thu, 15 Dec 2016 14:57:28 +0000 Subject: Update html link to markdown link --- doc/workflow/importing/import_projects_from_gitlab_com.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/workflow/importing/import_projects_from_gitlab_com.md b/doc/workflow/importing/import_projects_from_gitlab_com.md index bbb39e1641c..b27125a44de 100644 --- a/doc/workflow/importing/import_projects_from_gitlab_com.md +++ b/doc/workflow/importing/import_projects_from_gitlab_com.md @@ -6,7 +6,7 @@ You can read more about GitLab support [here](http://docs.gitlab.com/ce/integrat To get to the importer page you need to go to "New project" page. >**Note:** -If you are interested in importing Wiki and Merge Request data to your new instance, you'll need to follow the instructions for [project export](../../user/project/settings/import_export.html) +If you are interested in importing Wiki and Merge Request data to your new instance, you'll need to follow the instructions for [project export](../../user/project/settings/import_export.md) ![New project page](gitlab_importer/new_project_page.png) -- cgit v1.2.1 From d0a028a9d98de259edb71a10d99adb342df127e4 Mon Sep 17 00:00:00 2001 From: Daniel Guilak Date: Sat, 24 Dec 2016 15:38:25 -0800 Subject: Add detail to backup_restore remote storage docs - Location in which to add `gitlab_rails` configurations - Reminder to run `gitlab-ctl reconfigure` after changing gitlab_rails --- doc/raketasks/backup_restore.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index f42bb6a81a2..3a4b8355187 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -91,7 +91,7 @@ In the example below we use Amazon S3 for storage, but Fog also lets you use for AWS, OpenStack Swift and Rackspace as well. A local driver is [also available](#uploading-to-locally-mounted-shares). -For omnibus packages: +For omnibus packages, add the following to `/etc/gitlab/gitlab.rb`: ```ruby gitlab_rails['backup_upload_connection'] = { @@ -106,6 +106,8 @@ gitlab_rails['backup_upload_connection'] = { gitlab_rails['backup_upload_remote_directory'] = 'my.s3.bucket' ``` +Make sure to run `sudo gitlab-ctl reconfigure` after editing `/etc/gitlab/gitlab.rb` to reflect the changes. + For installations from source: ```yaml -- cgit v1.2.1 From 1591c90f7ba40bb7e0c3bcdd8ecf6dc43d75c129 Mon Sep 17 00:00:00 2001 From: Semyon Pupkov Date: Mon, 2 Jan 2017 16:07:11 +0500 Subject: Move admin users spinach tests to rspec https://gitlab.com/gitlab-org/gitlab-ce/issues/23036 --- features/admin/users.feature | 65 ------------- features/steps/admin/users.rb | 167 -------------------------------- spec/features/admin/admin_users_spec.rb | 155 ++++++++++++++++++++++++----- 3 files changed, 132 insertions(+), 255 deletions(-) delete mode 100644 features/admin/users.feature delete mode 100644 features/steps/admin/users.rb diff --git a/features/admin/users.feature b/features/admin/users.feature deleted file mode 100644 index 6755645778a..00000000000 --- a/features/admin/users.feature +++ /dev/null @@ -1,65 +0,0 @@ -@admin -Feature: Admin Users - Background: - Given I sign in as an admin - And system has users - - Scenario: On Admin Users - Given I visit admin users page - Then I should see all users - - Scenario: Edit user and change username to non ascii char - When I visit admin users page - And Click edit - And Input non ascii char in username - And Click save - Then See username error message - And Not changed form action url - - Scenario: Show user attributes - Given user "Mike" with groups and projects - Given I visit admin users page - And click on "Mike" link - Then I should see user "Mike" details - - Scenario: Edit my user attributes - Given I visit admin users page - And click edit on my user - When I submit modified user - Then I see user attributes changed - - @javascript - Scenario: Remove users secondary email - Given I visit admin users page - And I view the user with secondary email - And I see the secondary email - When I click remove secondary email - Then I should not see secondary email anymore - - Scenario: Show user keys - Given user "Pete" with ssh keys - And I visit admin users page - And click on user "Pete" - And click on ssh keys tab - Then I should see key list - And I click on the key title - Then I should see key details - And I click on remove key - Then I should see the key removed - - Scenario: Show user identities - Given user "Pete" with twitter account - And I visit "Pete" identities page in admin - Then I should see twitter details - - Scenario: Update user identities - Given user "Pete" with twitter account - And I visit "Pete" identities page in admin - And I modify twitter identity - Then I should see twitter details updated - - Scenario: Remove user identities - Given user "Pete" with twitter account - And I visit "Pete" identities page in admin - And I remove twitter identity - Then I should not see twitter details diff --git a/features/steps/admin/users.rb b/features/steps/admin/users.rb deleted file mode 100644 index 8fb8a86d58b..00000000000 --- a/features/steps/admin/users.rb +++ /dev/null @@ -1,167 +0,0 @@ -class Spinach::Features::AdminUsers < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedAdmin - - before do - allow(Gitlab::OAuth::Provider).to receive(:providers).and_return([:twitter, :twitter_updated]) - allow_any_instance_of(ApplicationHelper).to receive(:user_omniauth_authorize_path).and_return(root_path) - end - - after do - allow(Gitlab::OAuth::Provider).to receive(:providers).and_call_original - allow_any_instance_of(ApplicationHelper).to receive(:user_omniauth_authorize_path).and_call_original - end - - step 'I should see all users' do - User.all.each do |user| - expect(page).to have_content user.name - end - end - - step 'Click edit' do - @user = User.first - find("#edit_user_#{@user.id}").click - end - - step 'Input non ascii char in username' do - fill_in 'user_username', with: "\u3042\u3044" - end - - step 'Click save' do - click_button("Save") - end - - step 'See username error message' do - page.within "#error_explanation" do - expect(page).to have_content "Username" - end - end - - step 'Not changed form action url' do - expect(page).to have_selector %(form[action="/admin/users/#{@user.username}"]) - end - - step 'I submit modified user' do - check :user_can_create_group - click_button 'Save' - end - - step 'I see user attributes changed' do - expect(page).to have_content 'Can create groups: Yes' - end - - step 'click edit on my user' do - find("#edit_user_#{current_user.id}").click - end - - step 'I view the user with secondary email' do - @user_with_secondary_email = User.last - @user_with_secondary_email.emails.new(email: "secondary@example.com") - @user_with_secondary_email.save - visit "/admin/users/#{@user_with_secondary_email.username}" - end - - step 'I see the secondary email' do - expect(page).to have_content "Secondary email: #{@user_with_secondary_email.emails.last.email}" - end - - step 'I click remove secondary email' do - find("#remove_email_#{@user_with_secondary_email.emails.last.id}").click - end - - step 'I should not see secondary email anymore' do - expect(page).not_to have_content "Secondary email:" - end - - step 'user "Mike" with groups and projects' do - user = create(:user, name: 'Mike') - - project = create(:empty_project) - project.team << [user, :developer] - - group = create(:group) - group.add_developer(user) - end - - step 'click on "Mike" link' do - click_link "Mike" - end - - step 'I should see user "Mike" details' do - expect(page).to have_content 'Account' - expect(page).to have_content 'Personal projects limit' - end - - step 'user "Pete" with ssh keys' do - user = create(:user, name: 'Pete') - create(:key, user: user, title: "ssh-rsa Key1", key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC4FIEBXGi4bPU8kzxMefudPIJ08/gNprdNTaO9BR/ndy3+58s2HCTw2xCHcsuBmq+TsAqgEidVq4skpqoTMB+Uot5Uzp9z4764rc48dZiI661izoREoKnuRQSsRqUTHg5wrLzwxlQbl1MVfRWQpqiz/5KjBC7yLEb9AbusjnWBk8wvC1bQPQ1uLAauEA7d836tgaIsym9BrLsMVnR4P1boWD3Xp1B1T/ImJwAGHvRmP/ycIqmKdSpMdJXwxcb40efWVj0Ibbe7ii9eeoLdHACqevUZi6fwfbymdow+FeqlkPoHyGg3Cu4vD/D8+8cRc7mE/zGCWcQ15Var83Tczour Key1") - create(:key, user: user, title: "ssh-rsa Key2", key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQSTWXhJAX/He+nG78MiRRRn7m0Pb0XbcgTxE0etArgoFoh9WtvDf36HG6tOSg/0UUNcp0dICsNAmhBKdncp6cIyPaXJTURPRAGvhI0/VDk4bi27bRnccGbJ/hDaUxZMLhhrzY0r22mjVf8PF6dvv5QUIQVm1/LeaWYsHHvLgiIjwrXirUZPnFrZw6VLREoBKG8uWvfSXw1L5eapmstqfsME8099oi+vWLR8MgEysZQmD28M73fgW4zek6LDQzKQyJx9nB+hJkKUDvcuziZjGmRFlNgSA2mguERwL1OXonD8WYUrBDGKroIvBT39zS5d9tQDnidEJZ9Y8gv5ViYP7x Key2") - end - - step 'click on user "Pete"' do - click_link 'Pete' - end - - step 'I should see key list' do - expect(page).to have_content 'ssh-rsa Key2' - expect(page).to have_content 'ssh-rsa Key1' - end - - step 'I click on the key title' do - click_link 'ssh-rsa Key2' - end - - step 'I should see key details' do - expect(page).to have_content 'ssh-rsa Key2' - expect(page).to have_content 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQSTWXhJAX/He+nG78MiRRRn7m0Pb0XbcgTxE0etArgoFoh9WtvDf36HG6tOSg/0UUNcp0dICsNAmhBKdncp6cIyPaXJTURPRAGvhI0/VDk4bi27bRnccGbJ/hDaUxZMLhhrzY0r22mjVf8PF6dvv5QUIQVm1/LeaWYsHHvLgiIjwrXirUZPnFrZw6VLREoBKG8uWvfSXw1L5eapmstqfsME8099oi+vWLR8MgEysZQmD28M73fgW4zek6LDQzKQyJx9nB+hJkKUDvcuziZjGmRFlNgSA2mguERwL1OXonD8WYUrBDGKroIvBT39zS5d9tQDnidEJZ9Y8gv5ViYP7x Key2' - end - - step 'I click on remove key' do - click_link 'Remove' - end - - step 'I should see the key removed' do - expect(page).not_to have_content 'ssh-rsa Key2' - end - - step 'user "Pete" with twitter account' do - @user = create(:user, name: 'Pete') - @user.identities.create!(extern_uid: '123456', provider: 'twitter') - end - - step 'I visit "Pete" identities page in admin' do - visit admin_user_identities_path(@user) - end - - step 'I should see twitter details' do - expect(page).to have_content 'Pete' - expect(page).to have_content 'twitter' - end - - step 'I modify twitter identity' do - find('.table').find(:link, 'Edit').click - fill_in 'identity_extern_uid', with: '654321' - select 'twitter_updated', from: 'identity_provider' - click_button 'Save changes' - end - - step 'I should see twitter details updated' do - expect(page).to have_content 'Pete' - expect(page).to have_content 'twitter_updated' - expect(page).to have_content '654321' - end - - step 'I remove twitter identity' do - click_link 'Delete' - end - - step 'I should not see twitter details' do - expect(page).to have_content 'Pete' - expect(page).not_to have_content 'twitter' - end - - step 'click on ssh keys tab' do - click_link 'SSH keys' - end -end diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index 55ffc6761f8..26f35f7055a 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -1,9 +1,13 @@ require 'spec_helper' -describe "Admin::Users", feature: true do +describe "Admin::Users", feature: true do include WaitForAjax - before { login_as :admin } + let!(:user) do + create(:omniauth_user, provider: 'twitter', extern_uid: '123456') + end + + let!(:current_user) { login_as :admin } describe "GET /admin/users" do before do @@ -15,8 +19,10 @@ describe "Admin::Users", feature: true do end it "has users list" do - expect(page).to have_content(@user.email) - expect(page).to have_content(@user.name) + expect(page).to have_content(current_user.email) + expect(page).to have_content(current_user.name) + expect(page).to have_content(user.email) + expect(page).to have_content(user.name) end describe 'Two-factor Authentication filters' do @@ -40,8 +46,6 @@ describe "Admin::Users", feature: true do end it 'counts users who have not enabled 2FA' do - create(:user) - visit admin_users_path page.within('.filter-two-factor-disabled small') do @@ -50,8 +54,6 @@ describe "Admin::Users", feature: true do end it 'filters by users who have not enabled 2FA' do - user = create(:user) - visit admin_users_path click_link '2FA Disabled' @@ -110,10 +112,10 @@ describe "Admin::Users", feature: true do describe "GET /admin/users/:id" do it "has user info" do visit admin_users_path - click_link @user.name + click_link user.name - expect(page).to have_content(@user.email) - expect(page).to have_content(@user.name) + expect(page).to have_content(user.email) + expect(page).to have_content(user.name) end describe 'Impersonation' do @@ -126,7 +128,7 @@ describe "Admin::Users", feature: true do end it 'does not show impersonate button for admin itself' do - visit admin_user_path(@user) + visit admin_user_path(current_user) expect(page).not_to have_content('Impersonate') end @@ -158,7 +160,7 @@ describe "Admin::Users", feature: true do it 'logs out of impersonated user back to original user' do find(:css, 'li.impersonation a').click - expect(page.find(:css, '.header-user .profile-link')['data-user']).to eql(@user.username) + expect(page.find(:css, '.header-user .profile-link')['data-user']).to eql(current_user.username) end it 'is redirected back to the impersonated users page in the admin after stopping' do @@ -171,15 +173,15 @@ describe "Admin::Users", feature: true do describe 'Two-factor Authentication status' do it 'shows when enabled' do - @user.update_attribute(:otp_required_for_login, true) + user.update_attribute(:otp_required_for_login, true) - visit admin_user_path(@user) + visit admin_user_path(user) expect_two_factor_status('Enabled') end it 'shows when disabled' do - visit admin_user_path(@user) + visit admin_user_path(user) expect_two_factor_status('Disabled') end @@ -194,9 +196,8 @@ describe "Admin::Users", feature: true do describe "GET /admin/users/:id/edit" do before do - @simple_user = create(:user) visit admin_users_path - click_link "edit_user_#{@simple_user.id}" + click_link "edit_user_#{user.id}" end it "has user edit page" do @@ -214,16 +215,29 @@ describe "Admin::Users", feature: true do click_button "Save changes" end - it "shows page with new data" do + it "shows page with new data" do expect(page).to have_content('bigbang@mail.com') expect(page).to have_content('Big Bang') end it "changes user entry" do - @simple_user.reload - expect(@simple_user.name).to eq('Big Bang') - expect(@simple_user.is_admin?).to be_truthy - expect(@simple_user.password_expires_at).to be <= Time.now + user.reload + expect(user.name).to eq('Big Bang') + expect(user.is_admin?).to be_truthy + expect(user.password_expires_at).to be <= Time.now + end + end + + describe 'update username to non ascii char' do + it do + fill_in 'user_username', with: '\u3042\u3044' + click_button('Save') + + page.within '#error_explanation' do + expect(page).to have_content('Username') + end + + expect(page).to have_selector(%(form[action="/admin/users/#{user.username}"])) end end end @@ -270,4 +284,99 @@ describe "Admin::Users", feature: true do expect(page).not_to have_selector('.group_member') end end + + describe 'show user attributes' do + it do + visit admin_users_path + + click_link user.name + + expect(page).to have_content 'Account' + expect(page).to have_content 'Personal projects limit' + end + end + + describe 'remove users secondary email', js: true do + let!(:secondary_email) do + create :email, email: 'secondary@example.com', user: user + end + + it do + visit admin_user_path(user.username) + + expect(page).to have_content("Secondary email: #{secondary_email.email}") + + find("#remove_email_#{secondary_email.id}").click + + expect(page).not_to have_content(secondary_email.email) + end + end + + describe 'show user keys' do + let!(:key1) do + create(:key, user: user, title: "ssh-rsa Key1", key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC4FIEBXGi4bPU8kzxMefudPIJ08/gNprdNTaO9BR/ndy3+58s2HCTw2xCHcsuBmq+TsAqgEidVq4skpqoTMB+Uot5Uzp9z4764rc48dZiI661izoREoKnuRQSsRqUTHg5wrLzwxlQbl1MVfRWQpqiz/5KjBC7yLEb9AbusjnWBk8wvC1bQPQ1uLAauEA7d836tgaIsym9BrLsMVnR4P1boWD3Xp1B1T/ImJwAGHvRmP/ycIqmKdSpMdJXwxcb40efWVj0Ibbe7ii9eeoLdHACqevUZi6fwfbymdow+FeqlkPoHyGg3Cu4vD/D8+8cRc7mE/zGCWcQ15Var83Tczour Key1") + end + + let!(:key2) do + create(:key, user: user, title: "ssh-rsa Key2", key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQSTWXhJAX/He+nG78MiRRRn7m0Pb0XbcgTxE0etArgoFoh9WtvDf36HG6tOSg/0UUNcp0dICsNAmhBKdncp6cIyPaXJTURPRAGvhI0/VDk4bi27bRnccGbJ/hDaUxZMLhhrzY0r22mjVf8PF6dvv5QUIQVm1/LeaWYsHHvLgiIjwrXirUZPnFrZw6VLREoBKG8uWvfSXw1L5eapmstqfsME8099oi+vWLR8MgEysZQmD28M73fgW4zek6LDQzKQyJx9nB+hJkKUDvcuziZjGmRFlNgSA2mguERwL1OXonD8WYUrBDGKroIvBT39zS5d9tQDnidEJZ9Y8gv5ViYP7x Key2") + end + + it do + visit admin_users_path + + click_link user.name + click_link 'SSH keys' + + expect(page).to have_content(key1.title) + expect(page).to have_content(key2.title) + + click_link key2.title + + expect(page).to have_content(key2.title) + expect(page).to have_content(key2.key) + + click_link 'Remove' + + expect(page).not_to have_content(key2.title) + end + end + + describe 'show user identities' do + it 'shows user identities' do + visit admin_user_identities_path(user) + + expect(page).to have_content(user.name) + expect(page).to have_content('twitter') + end + end + + describe 'update user identities' do + before do + allow(Gitlab::OAuth::Provider).to receive(:providers).and_return([:twitter, :twitter_updated]) + end + + it 'modifies twitter identity' do + visit admin_user_identities_path(user) + + find('.table').find(:link, 'Edit').click + fill_in 'identity_extern_uid', with: '654321' + select 'twitter_updated', from: 'identity_provider' + click_button 'Save changes' + + expect(page).to have_content(user.name) + expect(page).to have_content('twitter_updated') + expect(page).to have_content('654321') + end + end + + describe 'remove user with identities' do + it 'removes user with twitter identity' do + visit admin_user_identities_path(user) + + click_link 'Delete' + + expect(page).to have_content(user.name) + expect(page).not_to have_content('twitter') + end + end end -- cgit v1.2.1 From 74c817c7cb3514d9e5966e1d39170ea17c1ff088 Mon Sep 17 00:00:00 2001 From: Semyon Pupkov Date: Mon, 2 Jan 2017 16:20:22 +0500 Subject: Tiny refactoring example in admin users feature spec --- spec/features/admin/admin_users_spec.rb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index 26f35f7055a..a586f8d3184 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -243,30 +243,30 @@ describe "Admin::Users", feature: true do end describe "GET /admin/users/:id/projects" do + let(:group) { create(:group) } + let!(:project) { create(:project, group: group) } + before do - @group = create(:group) - @project = create(:project, group: @group) - @simple_user = create(:user) - @group.add_developer(@simple_user) + group.add_developer(user) - visit projects_admin_user_path(@simple_user) + visit projects_admin_user_path(user) end it "lists group projects" do within(:css, '.append-bottom-default + .panel') do expect(page).to have_content 'Group projects' - expect(page).to have_link @group.name, admin_group_path(@group) + expect(page).to have_link group.name, admin_group_path(group) end end it 'allows navigation to the group details' do within(:css, '.append-bottom-default + .panel') do - click_link @group.name + click_link group.name end within(:css, 'h3.page-title') do - expect(page).to have_content "Group: #{@group.name}" + expect(page).to have_content "Group: #{group.name}" end - expect(page).to have_content @project.name + expect(page).to have_content project.name end it 'shows the group access level' do -- cgit v1.2.1 From 2eb0beb661cf1a97e3f36ae2785bb1ad44afd5d0 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 22 Dec 2016 14:31:42 -0600 Subject: add natural sorting token for build names --- app/models/commit_status.rb | 6 ++++++ app/views/projects/stage/_graph.html.haml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 31cd381dcd2..897e53fc7bd 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -137,4 +137,10 @@ class CommitStatus < ActiveRecord::Base .new(self, current_user) .fabricate! end + + def natsort_name + name.split(/(\d+)/).map do |v| + v =~ /\d+/ ? v.to_i : v + end + end end diff --git a/app/views/projects/stage/_graph.html.haml b/app/views/projects/stage/_graph.html.haml index d9d392fa02f..faadcfee30c 100644 --- a/app/views/projects/stage/_graph.html.haml +++ b/app/views/projects/stage/_graph.html.haml @@ -1,6 +1,6 @@ - stage = local_assigns.fetch(:stage) - statuses = stage.statuses.latest -- status_groups = statuses.sort_by(&:name).group_by(&:group_name) +- status_groups = statuses.sort_by(&:natsort_name).group_by(&:group_name) %li.stage-column .stage-name %a{ name: stage.name } -- cgit v1.2.1 From 19c7fc75a8aae79214fec0d71e5bdd54a58f5964 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 22 Dec 2016 14:31:46 -0600 Subject: add tests for natural sorting of build names --- .../projects/pipelines/show.html.haml_spec.rb | 35 ++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/spec/views/projects/pipelines/show.html.haml_spec.rb b/spec/views/projects/pipelines/show.html.haml_spec.rb index a066ea078e6..afda286c089 100644 --- a/spec/views/projects/pipelines/show.html.haml_spec.rb +++ b/spec/views/projects/pipelines/show.html.haml_spec.rb @@ -45,6 +45,41 @@ describe 'projects/pipelines/show' do expect(rendered).to have_text('jenkins') end + it 'lists builds in the correct sort order' do + create_build('test', 1, 'karma 0 20', :created) + create_build('test', 1, 'karma 12 20', :created) + create_build('test', 1, 'karma 1 20', :created) + create_build('test', 1, 'karma 10 20', :created) + create_build('test', 1, 'karma 11 20', :created) + create_build('test', 1, 'karma 2 20', :created) + create_build('test', 1, 'test 1.10', :created) + create_build('test', 1, 'test 1.5.1', :created) + create_build('test', 1, 'test 1 a', :created) + + render + + # spaced builds order + expected_order_1 = [ + 'karma 0 20', + 'karma 1 20', + 'karma 2 20', + 'karma 10 20', + 'karma 11 20', + 'karma 12 20' + ].join(' ') + + expect(rendered).to have_text(expected_order_1) + + # decimal builds order + expected_order_2 = [ + 'test 1 a', + 'test 1.5.1', + 'test 1.10' + ].join(' ') + + expect(rendered).to have_text(expected_order_2) + end + private def create_build(stage, stage_idx, name, status) -- cgit v1.2.1 From 713e8cf2eb5f5b46a355f9d76a49076c3b459d1c Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 22 Dec 2016 14:39:39 -0600 Subject: add CHANGELOG.md entry for !8277 --- changelogs/unreleased/fix-build-sort-order.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/fix-build-sort-order.yml diff --git a/changelogs/unreleased/fix-build-sort-order.yml b/changelogs/unreleased/fix-build-sort-order.yml new file mode 100644 index 00000000000..a6d6371f69a --- /dev/null +++ b/changelogs/unreleased/fix-build-sort-order.yml @@ -0,0 +1,4 @@ +--- +title: Sort numbers in build names more intelligently +merge_request: 8277 +author: -- cgit v1.2.1 From 1c5a506588342bdb2a6390f14d333d41a5482f58 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Tue, 27 Dec 2016 09:59:42 -0600 Subject: rename sort method --- app/models/commit_status.rb | 2 +- app/views/projects/stage/_graph.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 897e53fc7bd..9547c57b2ae 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -138,7 +138,7 @@ class CommitStatus < ActiveRecord::Base .fabricate! end - def natsort_name + def sortable_name name.split(/(\d+)/).map do |v| v =~ /\d+/ ? v.to_i : v end diff --git a/app/views/projects/stage/_graph.html.haml b/app/views/projects/stage/_graph.html.haml index faadcfee30c..4ee30b023ac 100644 --- a/app/views/projects/stage/_graph.html.haml +++ b/app/views/projects/stage/_graph.html.haml @@ -1,6 +1,6 @@ - stage = local_assigns.fetch(:stage) - statuses = stage.statuses.latest -- status_groups = statuses.sort_by(&:natsort_name).group_by(&:group_name) +- status_groups = statuses.sort_by(&:sortable_name).group_by(&:group_name) %li.stage-column .stage-name %a{ name: stage.name } -- cgit v1.2.1 From dc3d40ff28e4087120d48a94db5bd3d3b32f164d Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 2 Jan 2017 11:42:08 -0600 Subject: prefer unit test on model over view test --- spec/models/commit_status_spec.rb | 22 ++++++++++++++ .../projects/pipelines/show.html.haml_spec.rb | 35 ---------------------- 2 files changed, 22 insertions(+), 35 deletions(-) diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 701f3323c0f..daabc804d16 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -243,4 +243,26 @@ describe CommitStatus, models: true do .to be_a Gitlab::Ci::Status::Success end end + + describe '#sortable_name' do + subject { commit_status.sortable_name } + + tests = { + 'karma' => ['karma'], + 'karma 0 20' => ['karma ', 0, ' ', 20], + 'karma 10 20' => ['karma ', 10, ' ', 20], + 'karma 50:100' => ['karma ', 50, ':', 100], + 'karma 1.10' => ['karma ', 1, '.', 10], + 'karma 1.5.1' => ['karma ', 1, '.', 5, '.', 1], + 'karma 1 a' => ['karma ', 1, ' a'] + } + + tests.each do |name, sortable_name| + it "'#{name}' sorts as '#{sortable_name}'" do + commit_status.name = name + + is_expected.to eq(sortable_name) + end + end + end end diff --git a/spec/views/projects/pipelines/show.html.haml_spec.rb b/spec/views/projects/pipelines/show.html.haml_spec.rb index afda286c089..a066ea078e6 100644 --- a/spec/views/projects/pipelines/show.html.haml_spec.rb +++ b/spec/views/projects/pipelines/show.html.haml_spec.rb @@ -45,41 +45,6 @@ describe 'projects/pipelines/show' do expect(rendered).to have_text('jenkins') end - it 'lists builds in the correct sort order' do - create_build('test', 1, 'karma 0 20', :created) - create_build('test', 1, 'karma 12 20', :created) - create_build('test', 1, 'karma 1 20', :created) - create_build('test', 1, 'karma 10 20', :created) - create_build('test', 1, 'karma 11 20', :created) - create_build('test', 1, 'karma 2 20', :created) - create_build('test', 1, 'test 1.10', :created) - create_build('test', 1, 'test 1.5.1', :created) - create_build('test', 1, 'test 1 a', :created) - - render - - # spaced builds order - expected_order_1 = [ - 'karma 0 20', - 'karma 1 20', - 'karma 2 20', - 'karma 10 20', - 'karma 11 20', - 'karma 12 20' - ].join(' ') - - expect(rendered).to have_text(expected_order_1) - - # decimal builds order - expected_order_2 = [ - 'test 1 a', - 'test 1.5.1', - 'test 1.10' - ].join(' ') - - expect(rendered).to have_text(expected_order_2) - end - private def create_build(stage, stage_idx, name, status) -- cgit v1.2.1 From 7bceb30af745ad005b5507fdf928395886f7a2c4 Mon Sep 17 00:00:00 2001 From: Semyon Pupkov Date: Tue, 3 Jan 2017 22:26:22 +0500 Subject: Remove duplicate let in cycle_analytics_spec --- spec/features/cycle_analytics_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb index e48a2b0c92e..0648c89a5c7 100644 --- a/spec/features/cycle_analytics_spec.rb +++ b/spec/features/cycle_analytics_spec.rb @@ -3,7 +3,6 @@ require 'spec_helper' feature 'Cycle Analytics', feature: true, js: true do include WaitForAjax - let(:project) { create(:project) } let(:user) { create(:user) } let(:guest) { create(:user) } let(:project) { create(:project) } -- cgit v1.2.1 From 9af81a5a0a366e04319db2d181d555c171db5b22 Mon Sep 17 00:00:00 2001 From: Semyon Pupkov Date: Tue, 3 Jan 2017 23:45:42 +0500 Subject: Move dashboard active tab spinach test to rspec https://gitlab.com/gitlab-org/gitlab-ce/issues/23036 --- features/dashboard/active_tab.feature | 24 ---------------- features/steps/dashboard/active_tab.rb | 5 ---- spec/features/dashboard/active_tab_spec.rb | 46 ++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 29 deletions(-) delete mode 100644 features/dashboard/active_tab.feature delete mode 100644 features/steps/dashboard/active_tab.rb create mode 100644 spec/features/dashboard/active_tab_spec.rb diff --git a/features/dashboard/active_tab.feature b/features/dashboard/active_tab.feature deleted file mode 100644 index bd883a0ebfa..00000000000 --- a/features/dashboard/active_tab.feature +++ /dev/null @@ -1,24 +0,0 @@ -@dashboard -Feature: Dashboard Active Tab - Background: - Given I sign in as a user - - Scenario: On Dashboard Home - Given I visit dashboard page - Then the active main tab should be Home - And no other main tabs should be active - - Scenario: On Dashboard Issues - Given I visit dashboard issues page - Then the active main tab should be Issues - And no other main tabs should be active - - Scenario: On Dashboard Merge Requests - Given I visit dashboard merge requests page - Then the active main tab should be Merge Requests - And no other main tabs should be active - - Scenario: On Dashboard Groups - Given I visit dashboard groups page - Then the active main tab should be Groups - And no other main tabs should be active diff --git a/features/steps/dashboard/active_tab.rb b/features/steps/dashboard/active_tab.rb deleted file mode 100644 index 04fe96cef22..00000000000 --- a/features/steps/dashboard/active_tab.rb +++ /dev/null @@ -1,5 +0,0 @@ -class Spinach::Features::DashboardActiveTab < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedSidebarActiveTab -end diff --git a/spec/features/dashboard/active_tab_spec.rb b/spec/features/dashboard/active_tab_spec.rb new file mode 100644 index 00000000000..7d59fcac517 --- /dev/null +++ b/spec/features/dashboard/active_tab_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +RSpec.describe 'Dashboard Active Tab', feature: true do + before do + login_as :user + end + + shared_examples 'page has active tab' do |title| + it "#{title} tab" do + expect(page).to have_selector('.nav-sidebar li.active', count: 1) + expect(find('.nav-sidebar li.active')).to have_content(title) + end + end + + context 'on dashboard projects' do + before do + visit dashboard_projects_path + end + + it_behaves_like 'page has active tab', 'Projects' + end + + context 'on dashboard issues' do + before do + visit issues_dashboard_path + end + + it_behaves_like 'page has active tab', 'Issues' + end + + context 'on dashboard merge requests' do + before do + visit merge_requests_dashboard_path + end + + it_behaves_like 'page has active tab', 'Merge Requests' + end + + context 'on dashboard groups' do + before do + visit dashboard_groups_path + end + + it_behaves_like 'page has active tab', 'Groups' + end +end -- cgit v1.2.1 From e393151d76c5847de46e8d956e6c5947e321e156 Mon Sep 17 00:00:00 2001 From: Semyon Pupkov Date: Wed, 4 Jan 2017 13:30:11 +0500 Subject: Move dashboard archived projects spinach tests to rspec https://gitlab.com/gitlab-org/gitlab-ce/issues/23036 --- features/dashboard/archived_projects.feature | 17 -------------- features/steps/dashboard/archived_projects.rb | 26 --------------------- spec/factories/projects.rb | 4 ++++ spec/features/dashboard/archived_projects_spec.rb | 28 +++++++++++++++++++++++ 4 files changed, 32 insertions(+), 43 deletions(-) delete mode 100644 features/dashboard/archived_projects.feature delete mode 100644 features/steps/dashboard/archived_projects.rb create mode 100644 spec/features/dashboard/archived_projects_spec.rb diff --git a/features/dashboard/archived_projects.feature b/features/dashboard/archived_projects.feature deleted file mode 100644 index bed9282f1c6..00000000000 --- a/features/dashboard/archived_projects.feature +++ /dev/null @@ -1,17 +0,0 @@ -@dashboard -Feature: Dashboard Archived Projects - Background: - Given I sign in as a user - And I own project "Shop" - And I own project "Forum" - And project "Forum" is archived - And I visit dashboard page - - Scenario: I should see non-archived projects on dashboard - Then I should see "Shop" project link - And I should not see "Forum" project link - - Scenario: I toggle show of archived projects on dashboard - When I click "Show archived projects" link - Then I should see "Shop" project link - And I should see "Forum" project link diff --git a/features/steps/dashboard/archived_projects.rb b/features/steps/dashboard/archived_projects.rb deleted file mode 100644 index 6510f8d9b32..00000000000 --- a/features/steps/dashboard/archived_projects.rb +++ /dev/null @@ -1,26 +0,0 @@ -class Spinach::Features::DashboardArchivedProjects < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedProject - - When 'project "Forum" is archived' do - project = Project.find_by(name: "Forum") - project.update_attribute(:archived, true) - end - - step 'I should see "Shop" project link' do - expect(page).to have_link "Shop" - end - - step 'I should not see "Forum" project link' do - expect(page).not_to have_link "Forum" - end - - step 'I should see "Forum" project link' do - expect(page).to have_link "Forum" - end - - step 'I click "Show archived projects" link' do - click_link "Show archived projects" - end -end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index f7fa834d7a2..1cdbe4fc9a5 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -24,6 +24,10 @@ FactoryGirl.define do visibility_level Gitlab::VisibilityLevel::PRIVATE end + trait :archived do + archived true + end + trait :access_requestable do request_access_enabled true end diff --git a/spec/features/dashboard/archived_projects_spec.rb b/spec/features/dashboard/archived_projects_spec.rb new file mode 100644 index 00000000000..038c1641be9 --- /dev/null +++ b/spec/features/dashboard/archived_projects_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +RSpec.describe 'Dashboard Archived Project', feature: true do + let(:user) { create :user } + let(:project) { create :project} + let(:archived_project) { create(:project, :archived) } + + before do + project.team << [user, :master] + archived_project.team << [user, :master] + + login_as(user) + + visit dashboard_projects_path + end + + it 'renders non archived projects' do + expect(page).to have_link(project.name) + expect(page).not_to have_link(archived_project.name) + end + + it 'renders all projects' do + click_link 'Show archived projects' + + expect(page).to have_link(project.name) + expect(page).to have_link(archived_project.name) + end +end -- cgit v1.2.1 From 65b1ea3ba574c2283fa5a4a744861d8f78a992f8 Mon Sep 17 00:00:00 2001 From: Semyon Pupkov Date: Wed, 4 Jan 2017 15:27:29 +0500 Subject: Move dashboard group spinach test to rspec https://gitlab.com/gitlab-org/gitlab-ce/issues/23036 --- features/dashboard/group.feature | 13 ------------- features/steps/dashboard/group.rb | 25 ------------------------- spec/features/dashboard/group_spec.rb | 20 ++++++++++++++++++++ 3 files changed, 20 insertions(+), 38 deletions(-) delete mode 100644 features/dashboard/group.feature delete mode 100644 features/steps/dashboard/group.rb create mode 100644 spec/features/dashboard/group_spec.rb diff --git a/features/dashboard/group.feature b/features/dashboard/group.feature deleted file mode 100644 index 3ae2c679dc1..00000000000 --- a/features/dashboard/group.feature +++ /dev/null @@ -1,13 +0,0 @@ -@dashboard -Feature: Dashboard Group - Background: - Given I sign in as "John Doe" - And "John Doe" is owner of group "Owned" - And "John Doe" is guest of group "Guest" - - Scenario: Create a group from dasboard - And I visit dashboard groups page - And I click new group link - And submit form with new group "Samurai" info - Then I should be redirected to group "Samurai" page - And I should see newly created group "Samurai" diff --git a/features/steps/dashboard/group.rb b/features/steps/dashboard/group.rb deleted file mode 100644 index cf679fea530..00000000000 --- a/features/steps/dashboard/group.rb +++ /dev/null @@ -1,25 +0,0 @@ -class Spinach::Features::DashboardGroup < Spinach::FeatureSteps - include SharedAuthentication - include SharedGroup - include SharedPaths - include SharedUser - - step 'I click new group link' do - click_link "New Group" - end - - step 'submit form with new group "Samurai" info' do - fill_in 'group_path', with: 'Samurai' - fill_in 'group_description', with: 'Tokugawa Shogunate' - click_button "Create group" - end - - step 'I should be redirected to group "Samurai" page' do - expect(current_path).to eq group_path(Group.find_by(name: 'Samurai')) - end - - step 'I should see newly created group "Samurai"' do - expect(page).to have_content "Samurai" - expect(page).to have_content "Tokugawa Shogunate" - end -end diff --git a/spec/features/dashboard/group_spec.rb b/spec/features/dashboard/group_spec.rb new file mode 100644 index 00000000000..d5f8470fab0 --- /dev/null +++ b/spec/features/dashboard/group_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +RSpec.describe 'Dashboard Group', feature: true do + before do + login_as(:user) + end + + it 'creates new grpup' do + visit dashboard_groups_path + click_link 'New Group' + + fill_in 'group_path', with: 'Samurai' + fill_in 'group_description', with: 'Tokugawa Shogunate' + click_button 'Create group' + + expect(current_path).to eq group_path(Group.find_by(name: 'Samurai')) + expect(page).to have_content('Samurai') + expect(page).to have_content('Tokugawa Shogunate') + end +end -- cgit v1.2.1 From b9d0eeb5301fefddf71a73d04f237279fdcdea5b Mon Sep 17 00:00:00 2001 From: Semyon Pupkov Date: Wed, 4 Jan 2017 15:46:26 +0500 Subject: Move dashboard help spinach test to rspec https://gitlab.com/gitlab-org/gitlab-ce/issues/23036 --- features/dashboard/help.feature | 9 --------- features/steps/dashboard/help.rb | 21 --------------------- spec/features/dashboard/help_spec.rb | 17 +++++++++++++++++ 3 files changed, 17 insertions(+), 30 deletions(-) delete mode 100644 features/dashboard/help.feature delete mode 100644 features/steps/dashboard/help.rb create mode 100644 spec/features/dashboard/help_spec.rb diff --git a/features/dashboard/help.feature b/features/dashboard/help.feature deleted file mode 100644 index bca2772897b..00000000000 --- a/features/dashboard/help.feature +++ /dev/null @@ -1,9 +0,0 @@ -@dashboard -Feature: Dashboard Help - Background: - Given I sign in as a user - And I visit the "Rake Tasks" help page - - Scenario: The markdown should be rendered correctly - Then I should see "Rake Tasks" page markdown rendered - And Header "Rebuild project satellites" should have correct ids and links diff --git a/features/steps/dashboard/help.rb b/features/steps/dashboard/help.rb deleted file mode 100644 index 3c5bf44c538..00000000000 --- a/features/steps/dashboard/help.rb +++ /dev/null @@ -1,21 +0,0 @@ -class Spinach::Features::DashboardHelp < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedMarkdown - - step 'I visit the help page' do - visit help_path - end - - step 'I visit the "Rake Tasks" help page' do - visit help_page_path("administration/raketasks/maintenance") - end - - step 'I should see "Rake Tasks" page markdown rendered' do - expect(page).to have_content "Gather information about GitLab and the system it runs on" - end - - step 'Header "Rebuild project satellites" should have correct ids and links' do - header_should_have_correct_id_and_link(2, 'Check GitLab configuration', 'check-gitlab-configuration', '.documentation') - end -end diff --git a/spec/features/dashboard/help_spec.rb b/spec/features/dashboard/help_spec.rb new file mode 100644 index 00000000000..2803f7ec62b --- /dev/null +++ b/spec/features/dashboard/help_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +RSpec.describe 'Dashboard Help', feature: true do + before do + login_as(:user) + end + + it 'renders correctly markdown' do + visit help_page_path("administration/raketasks/maintenance") + + expect(page).to have_content('Gather information about GitLab and the system it runs on') + + node = find('.documentation h2 a#user-content-check-gitlab-configuration') + expect(node[:href]).to eq '#check-gitlab-configuration' + expect(find(:xpath, "#{node.path}/..").text).to eq 'Check GitLab configuration' + end +end -- cgit v1.2.1 From 22a34c81aa3801e5b453fd2c0a9364917e0aedbe Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Thu, 5 Jan 2017 13:46:26 +0100 Subject: Use configured git rather than system git --- app/models/cycle_analytics/summary.rb | 2 +- lib/gitlab/git/blame.rb | 2 +- lib/gitlab/git/repository.rb | 6 +++--- lib/tasks/gitlab/git.rake | 8 ++++---- spec/support/seed_helper.rb | 10 +++++----- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/models/cycle_analytics/summary.rb b/app/models/cycle_analytics/summary.rb index 82f53d17ddd..c9910d8cd09 100644 --- a/app/models/cycle_analytics/summary.rb +++ b/app/models/cycle_analytics/summary.rb @@ -31,7 +31,7 @@ class CycleAnalytics repository = @project.repository.raw_repository sha = @project.repository.commit(ref).sha - cmd = %W(git --git-dir=#{repository.path} log) + cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{repository.path} log) cmd << '--format=%H' cmd << "--after=#{@from.iso8601}" cmd << sha diff --git a/lib/gitlab/git/blame.rb b/lib/gitlab/git/blame.rb index 46f3969b6e1..2913230e979 100644 --- a/lib/gitlab/git/blame.rb +++ b/lib/gitlab/git/blame.rb @@ -27,7 +27,7 @@ module Gitlab private def load_blame - cmd = %W(git --git-dir=#{@repo.path} blame -p #{@sha} -- #{@path}) + cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{@repo.path} blame -p #{@sha} -- #{@path}) # Read in binary mode to ensure ASCII-8BIT raw_output = IO.popen(cmd, 'rb') {|io| io.read } output = encode_utf8(raw_output) diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 963b326a730..79b23d59b3a 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -332,7 +332,7 @@ module Gitlab end def log_by_shell(sha, options) - cmd = %W(git --git-dir=#{path} log) + cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} log) cmd += %W(-n #{options[:limit].to_i}) cmd += %w(--format=%H) cmd += %W(--skip=#{options[:offset].to_i}) @@ -913,7 +913,7 @@ module Gitlab return [] end - cmd = %W(git --git-dir=#{path} ls-tree) + cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} ls-tree) cmd += %w(-r) cmd += %w(--full-tree) cmd += %w(--full-name) @@ -1108,7 +1108,7 @@ module Gitlab end def archive_to_file(treeish = 'master', filename = 'archive.tar.gz', format = nil, compress_cmd = %w(gzip -n)) - git_archive_cmd = %W(git --git-dir=#{path} archive) + git_archive_cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} archive) # Put files into a directory before archiving prefix = "#{archive_name(treeish)}/" diff --git a/lib/tasks/gitlab/git.rake b/lib/tasks/gitlab/git.rake index f9834a4dae8..a67c1fe1f27 100644 --- a/lib/tasks/gitlab/git.rake +++ b/lib/tasks/gitlab/git.rake @@ -3,7 +3,7 @@ namespace :gitlab do desc "GitLab | Git | Repack" task repack: :environment do - failures = perform_git_cmd(%W(git repack -a --quiet), "Repacking repo") + failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} repack -a --quiet), "Repacking repo") if failures.empty? puts "Done".color(:green) else @@ -13,17 +13,17 @@ namespace :gitlab do desc "GitLab | Git | Run garbage collection on all repos" task gc: :environment do - failures = perform_git_cmd(%W(git gc --auto --quiet), "Garbage Collecting") + failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} gc --auto --quiet), "Garbage Collecting") if failures.empty? puts "Done".color(:green) else output_failures(failures) end end - + desc "GitLab | Git | Prune all repos" task prune: :environment do - failures = perform_git_cmd(%W(git prune), "Git Prune") + failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} prune), "Git Prune") if failures.empty? puts "Done".color(:green) else diff --git a/spec/support/seed_helper.rb b/spec/support/seed_helper.rb index 3f8398a31e3..03fa0a66b9a 100644 --- a/spec/support/seed_helper.rb +++ b/spec/support/seed_helper.rb @@ -25,32 +25,32 @@ module SeedHelper end def create_bare_seeds - system(git_env, *%W(git clone --bare #{GITLAB_URL}), + system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{GITLAB_URL}), chdir: SEED_REPOSITORY_PATH, out: '/dev/null', err: '/dev/null') end def create_normal_seeds - system(git_env, *%W(git clone #{TEST_REPO_PATH} #{TEST_NORMAL_REPO_PATH}), + system(git_env, *%W(#{Gitlab.config.git.bin_path} clone #{TEST_REPO_PATH} #{TEST_NORMAL_REPO_PATH}), out: '/dev/null', err: '/dev/null') end def create_mutable_seeds - system(git_env, *%W(git clone #{TEST_REPO_PATH} #{TEST_MUTABLE_REPO_PATH}), + system(git_env, *%W(#{Gitlab.config.git.bin_path} clone #{TEST_REPO_PATH} #{TEST_MUTABLE_REPO_PATH}), out: '/dev/null', err: '/dev/null') system(git_env, *%w(git branch -t feature origin/feature), chdir: TEST_MUTABLE_REPO_PATH, out: '/dev/null', err: '/dev/null') - system(git_env, *%W(git remote add expendable #{GITLAB_URL}), + system(git_env, *%W(#{Gitlab.config.git.bin_path} remote add expendable #{GITLAB_URL}), chdir: TEST_MUTABLE_REPO_PATH, out: '/dev/null', err: '/dev/null') end def create_broken_seeds - system(git_env, *%W(git clone --bare #{TEST_REPO_PATH} #{TEST_BROKEN_REPO_PATH}), + system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{TEST_REPO_PATH} #{TEST_BROKEN_REPO_PATH}), out: '/dev/null', err: '/dev/null') -- cgit v1.2.1 From 6ef6f9f205b8bed4ff0af4a353e7b04e0ff0b345 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Wed, 4 Jan 2017 21:24:58 +0100 Subject: Do not check for valid url for .gitlab-ci.yml Until this commit, GitLab would check if the URL in your .gitlab-ci.yml would be valid and adressable. However, this approach is rather limited. To begin, the URL couldn't be http://example:$PORT/my-project, as the PORT value would be interpolated after checking if the URL was valid, which its not. Also this gem will limit the amount of protocols allowed. This commit doesn't check, at all, if the URL is valid. This is now the responsablilty of the user. In my opion this is right, as the user ultimately benefits from a correct URL, but also, its impossible to cover all cases. --- lib/gitlab/ci/config/entry/environment.rb | 1 - spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 13 +++++++++++++ spec/lib/gitlab/ci/config/entry/environment_spec.rb | 17 ----------------- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/lib/gitlab/ci/config/entry/environment.rb b/lib/gitlab/ci/config/entry/environment.rb index b7b4b91eb51..f7c530c7d9f 100644 --- a/lib/gitlab/ci/config/entry/environment.rb +++ b/lib/gitlab/ci/config/entry/environment.rb @@ -33,7 +33,6 @@ module Gitlab validates :url, length: { maximum: 255 }, - addressable_url: true, allow_nil: true validates :action, diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 62d68721574..f824e2e1efe 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -769,6 +769,19 @@ module Ci expect(builds.first[:environment]).to eq(environment[:name]) expect(builds.first[:options]).to include(environment: environment) end + + context 'the url has a port as variable' do + let(:environment) do + { name: 'production', + url: 'http://production.gitlab.com:$PORT' } + end + + it 'allows a variable for the port' do + expect(builds.size).to eq(1) + expect(builds.first[:environment]).to eq(environment[:name]) + expect(builds.first[:options]).to include(environment: environment) + end + end end context 'when no environment is specified' do diff --git a/spec/lib/gitlab/ci/config/entry/environment_spec.rb b/spec/lib/gitlab/ci/config/entry/environment_spec.rb index d97806295fb..2adbed2154f 100644 --- a/spec/lib/gitlab/ci/config/entry/environment_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/environment_spec.rb @@ -196,22 +196,5 @@ describe Gitlab::Ci::Config::Entry::Environment do end end end - - context 'when invalid URL is used' do - let(:config) { { name: 'test', url: 'invalid-example.gitlab.com' } } - - describe '#valid?' do - it 'is not valid' do - expect(entry).not_to be_valid - end - end - - describe '#errors?' do - it 'contains error about invalid URL' do - expect(entry.errors) - .to include "environment url must be a valid url" - end - end - end end end -- cgit v1.2.1 From 9db19ae090195b1b594c554679b9f9a0aefab4bc Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Thu, 5 Jan 2017 22:25:46 +0100 Subject: Add changelog [ci skip] --- changelogs/unreleased/zj-unadressable-url-variables.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/zj-unadressable-url-variables.yml diff --git a/changelogs/unreleased/zj-unadressable-url-variables.yml b/changelogs/unreleased/zj-unadressable-url-variables.yml new file mode 100644 index 00000000000..6c412bd0540 --- /dev/null +++ b/changelogs/unreleased/zj-unadressable-url-variables.yml @@ -0,0 +1,4 @@ +--- +title: Don't validate environment urls on .gitlab-ci.yml +merge_request: +author: -- cgit v1.2.1 From 0ff564347c00b277f494947c8cd6803aa547c68a Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Thu, 5 Jan 2017 15:07:13 -0600 Subject: Keep max width on MR content on side by side view --- app/assets/stylesheets/pages/issuable.scss | 49 +++++++++++++++++++++++ app/views/projects/issues/show.html.haml | 2 +- app/views/projects/merge_requests/_show.html.haml | 4 +- 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 0a8c037c402..3272a862b85 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -1,3 +1,52 @@ +// Limit MR description for side-by-side diff view +.fixed-width-container { + max-width: $limited-layout-width - ($gl-padding * 2); + margin-left: auto; + margin-right: auto; +} + +.limit-container-width { + .detail-page-header { + @extend .fixed-width-container; + } + + .issuable-details { + .detail-page-description, + .mr-source-target, + .mr-state-widget, + .merge-manually { + @extend .fixed-width-container; + } + + .merge-request-tabs-holder { + &.affix { + border-bottom: 1px solid $border-color; + + .nav-links { + border: 0; + } + } + + .container-fluid { + @extend .fixed-width-container; + } + } + } + + .merge-request-details { + .emoji-list-container { + @extend .fixed-width-container; + } + } + + .diffs { + .mr-version-controls, + .files-changed { + @extend .fixed-width-container; + } + } +} + .issuable-details { section { .issuable-discussion { diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 981bf640a6b..43141971231 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -1,4 +1,4 @@ -- @content_class = "limit-container-width" +- @content_class = "limit-container-width" unless fluid_layout - page_title "#{@issue.title} (#{@issue.to_reference})", "Issues" - page_description @issue.description - page_card_attributes @issue.card_attributes diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index d9a3220b002..9c22caac6b9 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -1,4 +1,4 @@ -- @content_class = "limit-container-width" +- @content_class = "limit-container-width" unless fluid_layout - page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests" - page_description @merge_request.description - page_card_attributes @merge_request.card_attributes @@ -47,7 +47,7 @@ = succeed '.' do = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" - .content-block.content-block-small + .content-block.content-block-small.emoji-list-container = render 'award_emoji/awards_block', awardable: @merge_request, inline: true - if @commits_count.nonzero? -- cgit v1.2.1 From 23316b4963da5dd0173498904a2cd428ed2b0778 Mon Sep 17 00:00:00 2001 From: Brian Neel Date: Thu, 5 Jan 2017 17:03:08 -0500 Subject: Update rubocop and rubocop-rspec to fix build errors --- .gitlab-ci.yml | 8 +- .rubocop_todo.yml | 655 +++++++++++++++++++++++++++++++++++++++++++----------- Gemfile | 4 +- Gemfile.lock | 8 +- 4 files changed, 534 insertions(+), 141 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e0e780e1e6b..68690ff33da 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -233,7 +233,13 @@ spinach 9 10 ruby21: *spinach-knapsack-ruby21 script: - bundle exec $CI_BUILD_NAME -rubocop: *exec +rubocop: + <<: *ruby-static-analysis + <<: *dedicated-runner + stage: test + script: + - bundle exec "rubocop --require rubocop-rspec" + rake haml_lint: *exec rake scss_lint: *exec rake brakeman: *exec diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 11b34fafa2a..2434e8c4b42 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,65 +1,106 @@ # This configuration was generated by -# `rubocop --auto-gen-config --exclude-limit 0` -# on 2016-10-04 13:16:20 +0200 using RuboCop version 0.43.0. +# `rubocop --auto-gen-config` +# on 2017-01-05 14:31:09 -0800 using RuboCop version 0.46.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 160 +# Offense count: 28 +# Configuration parameters: Include. +# Include: **/Gemfile, **/gems.rb +Bundler/OrderedGems: + Exclude: + - 'Gemfile' + +# Offense count: 174 Lint/AmbiguousRegexpLiteral: Enabled: false -# Offense count: 40 +# Offense count: 53 # Configuration parameters: AllowSafeAssignment. Lint/AssignmentInCondition: Enabled: false -# Offense count: 18 +# Offense count: 1 +Lint/EmptyWhen: + Exclude: + - 'lib/gitlab/diff/parser.rb' + +# Offense count: 20 Lint/HandleExceptions: Enabled: false -# Offense count: 2 +# Offense count: 1 Lint/Loop: - Enabled: false + Exclude: + - 'app/mailers/notify.rb' -# Offense count: 19 +# Offense count: 28 Lint/ShadowingOuterLocalVariable: Enabled: false -# Offense count: 9 +# Offense count: 10 # Cop supports --auto-correct. Lint/UnifiedInteger: - Enabled: false + Exclude: + - 'spec/lib/gitlab/metrics/method_call_spec.rb' + - 'spec/lib/gitlab/metrics/metric_spec.rb' + - 'spec/lib/gitlab/metrics/system_spec.rb' + - 'spec/lib/gitlab/metrics/transaction_spec.rb' + - 'spec/models/repository_spec.rb' -# Offense count: 13 +# Offense count: 21 # Cop supports --auto-correct. Lint/UnneededSplatExpansion: - Enabled: false - -# Offense count: 69 + Exclude: + - 'config/application.rb' + - 'lib/banzai/filter/gollum_tags_filter.rb' + - 'lib/bitbucket/page.rb' + - 'lib/gitlab/markup_helper.rb' + - 'spec/helpers/auth_helper_spec.rb' + - 'spec/models/project_wiki_spec.rb' + - 'spec/requests/api/projects_spec.rb' + - 'spec/requests/api/tags_spec.rb' + - 'spec/support/seed_helper.rb' + - 'spec/support/test_env.rb' + +# Offense count: 82 # Cop supports --auto-correct. # Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments. Lint/UnusedBlockArgument: Enabled: false -# Offense count: 144 +# Offense count: 173 # Cop supports --auto-correct. # Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods. Lint/UnusedMethodArgument: Enabled: false -# Offense count: 2 +# Offense count: 94 +# Configuration parameters: CountComments. +Metrics/BlockLength: + Max: 289 + +# Offense count: 3 # Cop supports --auto-correct. Performance/RedundantBlockCall: - Enabled: false + Exclude: + - 'app/controllers/application_controller.rb' + - 'lib/gitlab/backend/shell.rb' + - 'lib/gitlab/optimistic_locking.rb' # Offense count: 5 # Cop supports --auto-correct. Performance/RedundantMatch: - Enabled: false + Exclude: + - 'app/models/external_issue.rb' + - 'lib/ci/api/helpers.rb' + - 'lib/extracts_path.rb' + - 'lib/gitlab/diff/highlight.rb' + - 'lib/gitlab/diff/parser.rb' -# Offense count: 26 +# Offense count: 32 # Cop supports --auto-correct. # Configuration parameters: MaxKeyValuePairs. Performance/RedundantMerge: @@ -67,63 +108,127 @@ Performance/RedundantMerge: # Offense count: 7 RSpec/BeEql: - Enabled: false + Exclude: + - 'spec/lib/gitlab/ldap/user_spec.rb' + - 'spec/lib/gitlab/o_auth/user_spec.rb' + - 'spec/lib/gitlab/saml/user_spec.rb' -# Offense count: 20 +# Offense count: 15 # Configuration parameters: CustomIncludeMethods. RSpec/EmptyExampleGroup: - Enabled: false + Exclude: + - 'spec/features/issues_spec.rb' + - 'spec/features/notes_on_merge_requests_spec.rb' + - 'spec/lib/gitlab/git_access_spec.rb' + - 'spec/models/project_snippet_spec.rb' + - 'spec/models/protected_branch_spec.rb' + - 'spec/routing/routing_spec.rb' -# Offense count: 16 +# Offense count: 24 RSpec/ExpectActual: - Enabled: false + Exclude: + - 'spec/lib/gitlab/regex_spec.rb' -# Offense count: 34 +# Offense count: 58 # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: implicit, each, example RSpec/HookArgument: Enabled: false -# Offense count: 168 +# Offense count: 12 +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: is_expected, should +RSpec/ImplicitExpect: + Exclude: + - 'spec/lib/banzai/renderer_spec.rb' + - 'spec/models/project_group_link_spec.rb' + - 'spec/models/project_services/external_wiki_service_spec.rb' + - 'spec/models/project_spec.rb' + +# Offense count: 235 RSpec/LeadingSubject: Enabled: false -# Offense count: 162 +# Offense count: 246 RSpec/LetSetup: Enabled: false -# Offense count: 10 +# Offense count: 13 RSpec/MessageChain: - Enabled: false - -# Offense count: 714 + Exclude: + - 'spec/finders/move_to_project_finder_spec.rb' + - 'spec/helpers/issues_helper_spec.rb' + - 'spec/initializers/secret_token_spec.rb' + - 'spec/models/build_spec.rb' + - 'spec/requests/api/internal_spec.rb' + - 'spec/services/merge_requests/build_service_spec.rb' + - 'spec/services/notes/create_service_spec.rb' + - 'spec/workers/emails_on_push_worker_spec.rb' + +# Offense count: 468 # Configuration parameters: EnforcedStyle, SupportedStyles. -# SupportedStyles: allow, expect -RSpec/MessageExpectation: +# SupportedStyles: have_received, receive +RSpec/MessageSpies: Enabled: false -# Offense count: 2423 +# Offense count: 3013 RSpec/MultipleExpectations: - Max: 36 + Max: 37 -# Offense count: 1504 +# Offense count: 2072 RSpec/NamedSubject: Enabled: false -# Offense count: 1335 +# Offense count: 1949 # Configuration parameters: MaxNesting. RSpec/NestedGroups: Enabled: false -# Offense count: 99 +# Offense count: 32 +RSpec/RepeatedDescription: + Exclude: + - 'spec/controllers/projects/todo_controller_spec.rb' + - 'spec/features/boards/boards_spec.rb' + - 'spec/helpers/issuables_helper_spec.rb' + - 'spec/lib/banzai/filter/emoji_filter_spec.rb' + - 'spec/models/commit_range_spec.rb' + - 'spec/models/global_milestone_spec.rb' + - 'spec/models/hooks/system_hook_spec.rb' + - 'spec/requests/api/commits_spec.rb' + - 'spec/requests/api/merge_requests_spec.rb' + - 'spec/requests/api/users_spec.rb' + - 'spec/requests/lfs_http_spec.rb' + - 'spec/routing/admin_routing_spec.rb' + - 'spec/services/notification_service_spec.rb' + +# Offense count: 1 +RSpec/SingleArgumentMessageChain: + Exclude: + - 'spec/requests/api/internal_spec.rb' + +# Offense count: 125 RSpec/SubjectStub: Enabled: false -# Offense count: 64 +# Offense count: 104 +# Cop supports --auto-correct. +# Configuration parameters: Whitelist. +# Whitelist: find_by_sql +Rails/DynamicFindBy: + Enabled: false + +# Offense count: 929 +# Cop supports --auto-correct. +# Configuration parameters: Include. +# Include: spec/**/*, test/**/* +Rails/HttpPositionalArguments: + Enabled: false + +# Offense count: 55 Rails/OutputSafety: Enabled: false -# Offense count: 151 +# Offense count: 181 # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: strict, flexible Rails/TimeZone: @@ -134,35 +239,52 @@ Rails/TimeZone: # Configuration parameters: Include. # Include: app/models/**/*.rb Rails/Validation: - Enabled: false + Exclude: + - 'app/models/ci/build.rb' + - 'app/models/ci/pipeline.rb' + - 'app/models/ci/runner_project.rb' + - 'app/models/ci/trigger.rb' + - 'app/models/commit_status.rb' + - 'app/models/members/group_member.rb' + - 'app/models/members/project_member.rb' + - 'app/models/project.rb' + - 'app/models/protected_branch.rb' + - 'app/models/user.rb' -# Offense count: 2 +# Offense count: 8 # Cop supports --auto-correct. +# Configuration parameters: AutoCorrect. Security/JSONLoad: - Enabled: false + Exclude: + - 'db/migrate/20161019190736_migrate_sidekiq_queues_from_default.rb' + - 'db/migrate/20161024042317_migrate_mailroom_queue_from_default.rb' + - 'db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb' + - 'spec/migrations/migrate_process_commit_worker_jobs_spec.rb' + - 'spec/models/project_services/irker_service_spec.rb' + - 'spec/support/stub_gitlab_calls.rb' -# Offense count: 284 +# Offense count: 339 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. # SupportedStyles: with_first_parameter, with_fixed_indentation Style/AlignParameters: Enabled: false -# Offense count: 28 +# Offense count: 27 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: always, conditionals Style/AndOr: Enabled: false -# Offense count: 52 +# Offense count: 53 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: percent_q, bare_percent Style/BarePercentLiterals: Enabled: false -# Offense count: 291 +# Offense count: 354 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: braces, no_braces, context_dependent @@ -171,97 +293,156 @@ Style/BracesAroundHashParameters: # Offense count: 6 Style/CaseEquality: - Enabled: false + Exclude: + - 'app/helpers/auth_helper.rb' + - 'app/models/commit.rb' + - 'app/models/protected_branch.rb' + - 'app/services/projects/download_service.rb' + - 'config/initializers/trusted_proxies.rb' + - 'spec/features/calendar_spec.rb' -# Offense count: 26 +# Offense count: 37 # Cop supports --auto-correct. Style/ColonMethodCall: Enabled: false -# Offense count: 2 +# Offense count: 4 # Cop supports --auto-correct. # Configuration parameters: Keywords. # Keywords: TODO, FIXME, OPTIMIZE, HACK, REVIEW Style/CommentAnnotation: - Enabled: false + Exclude: + - 'config/routes/user.rb' + - 'lib/api/commits.rb' + - 'lib/api/entities.rb' + - 'spec/requests/api/project_snippets_spec.rb' -# Offense count: 30 +# Offense count: 29 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, SingleLineConditionsOnly. # SupportedStyles: assign_to_condition, assign_inside_condition Style/ConditionalAssignment: Enabled: false -# Offense count: 957 +# Offense count: 1210 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: leading, trailing Style/DotPosition: Enabled: false -# Offense count: 13 +# Offense count: 18 Style/DoubleNegation: - Enabled: false + Exclude: + - 'app/controllers/users_controller.rb' + - 'app/models/commit.rb' + - 'app/models/merge_request.rb' + - 'app/models/project.rb' + - 'app/models/project_services/buildkite_service.rb' + - 'app/models/project_services/drone_ci_service.rb' + - 'app/models/project_wiki.rb' + - 'app/models/user.rb' + - 'config/initializers/6_validations.rb' + - 'lib/banzai/filter/relative_link_filter.rb' + - 'lib/gitlab/git/diff_collection.rb' + - 'lib/gitlab/git/repository.rb' + - 'lib/gitlab/sherlock.rb' + - 'lib/gitlab/sherlock/middleware.rb' -# Offense count: 6 +# Offense count: 7 # Cop supports --auto-correct. Style/EachWithObject: - Enabled: false + Exclude: + - 'app/controllers/projects/cycle_analytics_controller.rb' + - 'app/services/merge_requests/get_urls_service.rb' + - 'lib/ci/ansi2html.rb' + - 'lib/expand_variables.rb' + - 'lib/gitlab/ee_compat_check.rb' + - 'lib/gitlab/import_export/members_mapper.rb' + - 'lib/gitlab/import_export/relation_factory.rb' -# Offense count: 26 +# Offense count: 24 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: empty, nil, both Style/EmptyElse: Enabled: false -# Offense count: 3 +# Offense count: 4 # Cop supports --auto-correct. Style/EmptyLiteral: + Exclude: + - 'features/steps/project/commits/commits.rb' + - 'lib/gitlab/fogbugz_import/importer.rb' + - 'lib/gitlab/git/diff_collection.rb' + - 'spec/lib/gitlab/workhorse_spec.rb' + +# Offense count: 57 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: compact, expanded +Style/EmptyMethod: Enabled: false -# Offense count: 140 +# Offense count: 145 # Cop supports --auto-correct. # Configuration parameters: AllowForAlignment, ForceEqualSignAlignment. Style/ExtraSpacing: Enabled: false -# Offense count: 6 +# Offense count: 8 # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: format, sprintf, percent Style/FormatString: - Enabled: false - -# Offense count: 201 + Exclude: + - 'app/models/ci/pipeline.rb' + - 'app/services/gravatar_service.rb' + - 'config/initializers/rack_lineprof.rb' + - 'lib/gitlab/version_info.rb' + - 'spec/factories/lfs_objects.rb' + - 'spec/services/issues/move_service_spec.rb' + +# Offense count: 236 # Configuration parameters: MinBodyLength. Style/GuardClause: Enabled: false # Offense count: 11 Style/IfInsideElse: - Enabled: false - -# Offense count: 174 + Exclude: + - 'app/controllers/projects/forks_controller.rb' + - 'app/controllers/projects/imports_controller.rb' + - 'app/helpers/milestones_helper.rb' + - 'app/helpers/projects_helper.rb' + - 'app/services/system_note_service.rb' + - 'config/initializers/1_settings.rb' + - 'lib/gitlab/auth.rb' + - 'lib/gitlab/checks/change_access.rb' + - 'lib/gitlab/database.rb' + - 'spec/controllers/projects/merge_requests_controller_spec.rb' + - 'spec/support/import_export/export_file_helper.rb' + +# Offense count: 173 # Cop supports --auto-correct. # Configuration parameters: MaxLineLength. Style/IfUnlessModifier: Enabled: false -# Offense count: 53 +# Offense count: 55 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. # SupportedStyles: special_inside_parentheses, consistent, align_brackets Style/IndentArray: Enabled: false -# Offense count: 95 +# Offense count: 99 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. # SupportedStyles: special_inside_parentheses, consistent, align_braces Style/IndentHash: Enabled: false -# Offense count: 29 +# Offense count: 41 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: line_count_dependent, lambda, literal @@ -271,18 +452,49 @@ Style/Lambda: # Offense count: 5 # Cop supports --auto-correct. Style/LineEndConcatenation: - Enabled: false + Exclude: + - 'app/helpers/preferences_helper.rb' + - 'app/helpers/tree_helper.rb' + - 'app/models/merge_request.rb' + - 'spec/lib/gitlab/gfm/reference_rewriter_spec.rb' -# Offense count: 15 +# Offense count: 19 # Cop supports --auto-correct. Style/MethodCallParentheses: - Enabled: false + Exclude: + - 'lib/ci/ansi2html.rb' + - 'spec/controllers/dashboard/todos_controller_spec.rb' + - 'spec/controllers/projects/issues_controller_spec.rb' + - 'spec/controllers/projects/merge_requests_controller_spec.rb' + - 'spec/controllers/projects/snippets_controller_spec.rb' + - 'spec/features/calendar_spec.rb' + - 'spec/features/dashboard/datetime_on_tooltips_spec.rb' + - 'spec/helpers/submodule_helper_spec.rb' + - 'spec/lib/gitlab/git/repository_spec.rb' + - 'spec/requests/api/internal_spec.rb' -# Offense count: 8 +# Offense count: 9 Style/MethodMissing: - Enabled: false + Exclude: + - 'app/controllers/projects/application_controller.rb' + - 'app/models/commit.rb' + - 'app/models/network/commit.rb' + - 'app/models/repository.rb' + - 'config/initializers/gollum.rb' + - 'lib/bitbucket/collection.rb' + - 'lib/gitlab/github_import/client.rb' + - 'lib/gitlab/lazy.rb' + - 'lib/uploaded_file.rb' -# Offense count: 95 +# Offense count: 3 +# Cop supports --auto-correct. +Style/MultilineIfModifier: + Exclude: + - 'app/helpers/snippets_helper.rb' + - 'app/models/merge_request.rb' + - 'app/models/project_wiki.rb' + +# Offense count: 178 # Cop supports --auto-correct. Style/MutableConstant: Enabled: false @@ -290,53 +502,95 @@ Style/MutableConstant: # Offense count: 8 # Cop supports --auto-correct. Style/NestedParenthesizedCalls: - Enabled: false + Exclude: + - 'app/helpers/commits_helper.rb' + - 'app/workers/irker_worker.rb' + - 'spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb' + - 'spec/lib/gitlab/email/message/repository_push_spec.rb' # Offense count: 13 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles. # SupportedStyles: skip_modifier_ifs, always Style/Next: - Enabled: false + Exclude: + - 'app/models/namespace.rb' + - 'app/models/user.rb' + - 'config/initializers/secret_token.rb' + - 'db/migrate/20161220141214_remove_dot_git_from_group_names.rb' + - 'db/migrate/20161226122833_remove_dot_git_from_usernames.rb' + - 'lib/banzai/filter/math_filter.rb' + - 'lib/banzai/filter/table_of_contents_filter.rb' + - 'lib/gitlab/fogbugz_import/importer.rb' + - 'lib/gitlab/metrics/instrumentation.rb' + - 'lib/gitlab/metrics/sampler.rb' + - 'lib/gitlab/project_search_results.rb' -# Offense count: 12 +# Offense count: 19 # Cop supports --auto-correct. # Configuration parameters: EnforcedOctalStyle, SupportedOctalStyles. # SupportedOctalStyles: zero_with_o, zero_only Style/NumericLiteralPrefix: - Enabled: false + Exclude: + - 'config/initializers/1_settings.rb' + - 'config/initializers/secret_token.rb' + - 'lib/gitlab/backend/shell.rb' + - 'lib/gitlab/import_export/command_line_util.rb' + - 'lib/gitlab/workhorse.rb' + - 'spec/features/projects/import_export/export_file_spec.rb' + - 'spec/lib/gitlab/git/blob_spec.rb' + - 'spec/lib/gitlab/git/hook_spec.rb' + - 'spec/lib/gitlab/git/repository_spec.rb' + - 'spec/lib/gitlab/workhorse_spec.rb' + - 'spec/support/import_export/export_file_helper.rb' + - 'spec/support/test_env.rb' + - 'spec/tasks/gitlab/backup_rake_spec.rb' -# Offense count: 53 +# Offense count: 19 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. +# Configuration parameters: AutoCorrect, EnforcedStyle, SupportedStyles. # SupportedStyles: predicate, comparison Style/NumericPredicate: Enabled: false -# Offense count: 29 +# Offense count: 34 # Cop supports --auto-correct. Style/ParallelAssignment: Enabled: false -# Offense count: 294 +# Offense count: 416 # Cop supports --auto-correct. # Configuration parameters: PreferredDelimiters. Style/PercentLiteralDelimiters: Enabled: false -# Offense count: 11 +# Offense count: 10 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: lower_case_q, upper_case_q Style/PercentQLiterals: - Enabled: false + Exclude: + - 'spec/lib/gitlab/diff/highlight_spec.rb' + - 'spec/models/project_services/bamboo_service_spec.rb' + - 'spec/models/project_services/teamcity_service_spec.rb' + - 'spec/workers/repository_import_worker_spec.rb' # Offense count: 13 # Cop supports --auto-correct. Style/PerlBackrefs: - Enabled: false - -# Offense count: 38 + Exclude: + - 'app/controllers/projects/application_controller.rb' + - 'app/helpers/submodule_helper.rb' + - 'lib/banzai/filter/abstract_reference_filter.rb' + - 'lib/banzai/filter/autolink_filter.rb' + - 'lib/banzai/filter/emoji_filter.rb' + - 'lib/banzai/filter/gollum_tags_filter.rb' + - 'lib/expand_variables.rb' + - 'lib/gitlab/diff/highlight.rb' + - 'lib/gitlab/search_results.rb' + - 'lib/gitlab/sherlock/query.rb' + +# Offense count: 63 # Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist. # NamePrefix: is_, has_, have_ # NamePrefixBlacklist: is_, has_, have_ @@ -344,17 +598,25 @@ Style/PerlBackrefs: Style/PredicateName: Enabled: false -# Offense count: 26 +# Offense count: 33 # Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: short, verbose Style/PreferredHashMethods: Enabled: false -# Offense count: 6 +# Offense count: 8 # Cop supports --auto-correct. Style/Proc: - Enabled: false + Exclude: + - 'app/mailers/base_mailer.rb' + - 'app/models/milestone.rb' + - 'app/models/service.rb' + - 'lib/api/api_guard.rb' + - 'lib/gitlab/ldap/config.rb' + - 'spec/initializers/trusted_proxies_spec.rb' -# Offense count: 22 +# Offense count: 50 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: compact, exploded @@ -364,107 +626,196 @@ Style/RaiseArgs: # Offense count: 4 # Cop supports --auto-correct. Style/RedundantBegin: - Enabled: false + Exclude: + - 'app/models/ci/build.rb' + - 'app/models/merge_request.rb' + - 'app/services/projects/import_service.rb' + - 'lib/gitlab/git/commit.rb' # Offense count: 1 # Cop supports --auto-correct. Style/RedundantException: - Enabled: false + Exclude: + - 'app/helpers/preferences_helper.rb' -# Offense count: 24 +# Offense count: 29 # Cop supports --auto-correct. Style/RedundantFreeze: - Enabled: false + Exclude: + - 'app/models/merge_request.rb' + - 'app/validators/color_validator.rb' + - 'app/validators/line_code_validator.rb' + - 'lib/api/templates.rb' + - 'lib/banzai/filter/blockquote_fence_filter.rb' + - 'lib/banzai/filter/gollum_tags_filter.rb' + - 'lib/banzai/filter/yaml_front_matter_filter.rb' + - 'lib/gitlab/diff/inline_diff.rb' + - 'lib/gitlab/ee_compat_check.rb' + - 'lib/gitlab/import_export/relation_factory.rb' + - 'lib/gitlab/incoming_email.rb' + - 'lib/gitlab/middleware/static.rb' + - 'lib/gitlab/regex.rb' -# Offense count: 427 +# Offense count: 11 +# Cop supports --auto-correct. +# Configuration parameters: AllowMultipleReturnValues. +Style/RedundantReturn: + Exclude: + - 'app/controllers/concerns/issuable_actions.rb' + - 'app/controllers/groups/application_controller.rb' + - 'app/controllers/groups_controller.rb' + - 'app/controllers/projects/labels_controller.rb' + - 'app/controllers/projects/milestones_controller.rb' + - 'app/services/access_token_validation_service.rb' + +# Offense count: 359 # Cop supports --auto-correct. Style/RedundantSelf: Enabled: false -# Offense count: 97 +# Offense count: 105 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, AllowInnerSlashes. # SupportedStyles: slashes, percent_r, mixed Style/RegexpLiteral: Enabled: false -# Offense count: 18 +# Offense count: 19 # Cop supports --auto-correct. Style/RescueModifier: - Enabled: false - -# Offense count: 114 -# Cop supports --auto-correct. -Style/SafeNavigation: - Enabled: false + Exclude: + - 'app/controllers/admin/system_info_controller.rb' + - 'app/controllers/users_controller.rb' + - 'app/helpers/blob_helper.rb' + - 'app/helpers/issues_helper.rb' + - 'app/models/diff_note.rb' + - 'app/models/project_services/external_wiki_service.rb' + - 'app/models/sent_notification.rb' + - 'app/models/todo.rb' + - 'config/initializers/1_settings.rb' + - 'config/initializers/attr_encrypted_no_db_connection.rb' + - 'lib/gitlab/current_settings.rb' + - 'lib/gitlab/diff/parser.rb' + - 'lib/gitlab/kubernetes.rb' + - 'lib/gitlab/slash_commands/command_definition.rb' + - 'spec/support/matchers/be_url.rb' # Offense count: 7 # Cop supports --auto-correct. Style/SelfAssignment: - Enabled: false + Exclude: + - 'app/models/project_team.rb' + - 'app/services/notification_service.rb' + - 'lib/api/runners.rb' + - 'spec/features/merge_requests/diff_notes_resolve_spec.rb' + - 'spec/support/import_export/configuration_helper.rb' # Offense count: 2 # Configuration parameters: Methods. -# Methods: {"reduce"=>["a", "e"]}, {"inject"=>["a", "e"]} +# Methods: {"reduce"=>["acc", "elem"]}, {"inject"=>["acc", "elem"]} Style/SingleLineBlockParams: - Enabled: false + Exclude: + - 'app/models/commit.rb' + - 'spec/support/services_shared_context.rb' # Offense count: 50 # Cop supports --auto-correct. # Configuration parameters: AllowIfMethodIsEmpty. Style/SingleLineMethods: - Enabled: false + Exclude: + - 'lib/ci/ansi2html.rb' -# Offense count: 125 +# Offense count: 140 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: space, no_space Style/SpaceBeforeBlockBraces: Enabled: false -# Offense count: 10 +# Offense count: 8 # Cop supports --auto-correct. # Configuration parameters: AllowForAlignment. Style/SpaceBeforeFirstArg: - Enabled: false + Exclude: + - 'config/routes/project.rb' + - 'features/steps/project/source/browse_files.rb' + - 'features/steps/project/source/markdown_render.rb' + - 'spec/routing/project_routing_spec.rb' + - 'spec/services/system_note_service_spec.rb' -# Offense count: 145 +# Offense count: 37 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles. +# SupportedStyles: require_no_space, require_space +Style/SpaceInLambdaLiteral: + Exclude: + - 'app/models/concerns/mentionable.rb' + - 'app/models/issue.rb' + - 'app/models/member.rb' + - 'lib/api/entities.rb' + - 'lib/file_size_validator.rb' + - 'lib/gitlab/visibility_level.rb' + - 'spec/models/concerns/participable_spec.rb' + - 'spec/models/cycle_analytics/code_spec.rb' + - 'spec/models/cycle_analytics/issue_spec.rb' + - 'spec/models/cycle_analytics/plan_spec.rb' + - 'spec/models/cycle_analytics/production_spec.rb' + - 'spec/models/cycle_analytics/review_spec.rb' + - 'spec/models/cycle_analytics/staging_spec.rb' + - 'spec/models/cycle_analytics/test_spec.rb' + +# Offense count: 174 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters. # SupportedStyles: space, no_space Style/SpaceInsideBlockBraces: Enabled: false -# Offense count: 99 +# Offense count: 115 # Cop supports --auto-correct. Style/SpaceInsideBrackets: Enabled: false -# Offense count: 65 +# Offense count: 77 # Cop supports --auto-correct. Style/SpaceInsideParens: Enabled: false -# Offense count: 7 +# Offense count: 4 # Cop supports --auto-correct. Style/SpaceInsidePercentLiteralDelimiters: - Enabled: false + Exclude: + - 'lib/gitlab/git_access.rb' -# Offense count: 41 +# Offense count: 53 # Cop supports --auto-correct. # Configuration parameters: SupportedStyles. # SupportedStyles: use_perl_names, use_english_names Style/SpecialGlobalVars: EnforcedStyle: use_perl_names -# Offense count: 31 +# Offense count: 25 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: single_quotes, double_quotes Style/StringLiteralsInInterpolation: - Enabled: false + Exclude: + - 'app/helpers/labels_helper.rb' + - 'app/helpers/time_helper.rb' + - 'app/helpers/todos_helper.rb' + - 'app/models/application_setting.rb' + - 'app/models/project_services/campfire_service.rb' + - 'app/models/project_services/jira_service.rb' + - 'db/migrate/20161017125927_add_unique_index_to_labels.rb' + - 'features/steps/groups.rb' + - 'features/steps/project/merge_requests.rb' + - 'lib/banzai/filter/abstract_reference_filter.rb' + - 'lib/ci/gitlab_ci_yaml_processor.rb' + - 'lib/gitlab/google_code_import/importer.rb' + - 'spec/lib/banzai/filter/issue_reference_filter_spec.rb' + - 'spec/models/project_services/hipchat_service_spec.rb' -# Offense count: 33 +# Offense count: 52 # Cop supports --auto-correct. # Configuration parameters: IgnoredMethods. # IgnoredMethods: respond_to, define_method @@ -474,18 +825,23 @@ Style/SymbolProc: # Offense count: 5 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, AllowSafeAssignment. -# SupportedStyles: require_parentheses, require_no_parentheses +# SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex Style/TernaryParentheses: - Enabled: false + Exclude: + - 'features/support/capybara.rb' + - 'lib/api/projects.rb' + - 'lib/banzai/filter/table_of_contents_filter.rb' + - 'lib/gitlab/ci/build/artifacts/metadata/entry.rb' + - 'spec/support/capybara.rb' -# Offense count: 29 +# Offense count: 35 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyleForMultiline, SupportedStyles. # SupportedStyles: comma, consistent_comma, no_comma Style/TrailingCommaInArguments: Enabled: false -# Offense count: 102 +# Offense count: 149 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyleForMultiline, SupportedStyles. # SupportedStyles: comma, consistent_comma, no_comma @@ -496,31 +852,62 @@ Style/TrailingCommaInLiteral: # Cop supports --auto-correct. # Configuration parameters: AllowNamedUnderscoreVariables. Style/TrailingUnderscoreVariable: - Enabled: false + Exclude: + - 'app/controllers/admin/background_jobs_controller.rb' + - 'app/controllers/invites_controller.rb' + - 'app/controllers/projects/git_http_client_controller.rb' + - 'app/helpers/tab_helper.rb' + - 'lib/gitlab/git/repository.rb' + - 'lib/gitlab/logger.rb' -# Offense count: 76 +# Offense count: 66 # Cop supports --auto-correct. Style/TrailingWhitespace: Enabled: false -# Offense count: 2 +# Offense count: 3 # Cop supports --auto-correct. # Configuration parameters: ExactNameMatch, AllowPredicates, AllowDSLWriters, IgnoreClassMethods, Whitelist. # Whitelist: to_ary, to_a, to_c, to_enum, to_h, to_hash, to_i, to_int, to_io, to_open, to_path, to_proc, to_r, to_regexp, to_str, to_s, to_sym Style/TrivialAccessors: - Enabled: false + Exclude: + - 'app/models/external_issue.rb' + - 'app/serializers/base_serializer.rb' + - 'lib/gitlab/ldap/person.rb' # Offense count: 2 # Cop supports --auto-correct. Style/UnlessElse: - Enabled: false + Exclude: + - 'lib/gitlab/project_search_results.rb' + - 'spec/features/issues/award_emoji_spec.rb' -# Offense count: 14 +# Offense count: 17 # Cop supports --auto-correct. Style/UnneededInterpolation: - Enabled: false + Exclude: + - 'app/helpers/boards_helper.rb' + - 'app/helpers/projects_helper.rb' + - 'app/helpers/search_helper.rb' + - 'app/helpers/time_helper.rb' + - 'app/models/merge_request.rb' + - 'app/models/project_services/pushover_service.rb' + - 'features/steps/project/forked_merge_requests.rb' + - 'spec/features/admin/admin_projects_spec.rb' + - 'spec/features/issues_spec.rb' + - 'spec/features/projects/branches_spec.rb' + - 'spec/features/projects/issuable_templates_spec.rb' + - 'spec/lib/banzai/filter/issue_reference_filter_spec.rb' + - 'spec/views/projects/commit/_commit_box.html.haml_spec.rb' # Offense count: 8 # Cop supports --auto-correct. Style/ZeroLengthPredicate: - Enabled: false + Exclude: + - 'app/models/deploy_key.rb' + - 'app/models/merge_request.rb' + - 'app/models/network/commit.rb' + - 'app/models/network/graph.rb' + - 'app/models/project_services/asana_service.rb' + - 'app/models/repository.rb' + - 'lib/extracts_path.rb' diff --git a/Gemfile b/Gemfile index 2e8ad75fd71..29695c08e45 100644 --- a/Gemfile +++ b/Gemfile @@ -296,8 +296,8 @@ group :development, :test do gem 'spring-commands-spinach', '~> 1.1.0' gem 'spring-commands-teaspoon', '~> 0.0.2' - gem 'rubocop', '~> 0.43.0', require: false - gem 'rubocop-rspec', '~> 1.5.0', require: false + gem 'rubocop', '~> 0.46.0', require: false + gem 'rubocop-rspec', '~> 1.9.1', require: false gem 'scss_lint', '~> 0.47.0', require: false gem 'haml_lint', '~> 0.18.2', require: false gem 'simplecov', '0.12.0', require: false diff --git a/Gemfile.lock b/Gemfile.lock index c99313163a4..b042e4b1b09 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -614,14 +614,14 @@ GEM rspec-retry (0.4.5) rspec-core rspec-support (3.5.0) - rubocop (0.43.0) + rubocop (0.46.0) parser (>= 2.3.1.1, < 3.0) powerpack (~> 0.1) rainbow (>= 1.99.1, < 3.0) ruby-progressbar (~> 1.7) unicode-display_width (~> 1.0, >= 1.0.1) - rubocop-rspec (1.5.0) - rubocop (>= 0.40.0) + rubocop-rspec (1.9.1) + rubocop (>= 0.42.0) ruby-fogbugz (0.2.1) crack (~> 0.4) ruby-prof (0.16.2) @@ -933,7 +933,7 @@ DEPENDENCIES rspec-rails (~> 3.5.0) rspec-retry (~> 0.4.5) rubocop (~> 0.43.0) - rubocop-rspec (~> 1.5.0) + rubocop-rspec (~> 1.9.1) ruby-fogbugz (~> 0.2.1) ruby-prof (~> 0.16.2) rugged (~> 0.24.0) -- cgit v1.2.1 From 719294cbfe522f1706292dccb5f269503502b37d Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 9 Jan 2017 15:24:25 +0100 Subject: update templates --- vendor/gitignore/Android.gitignore | 2 + vendor/gitignore/Autotools.gitignore | 15 ++++ vendor/gitignore/CMake.gitignore | 1 + vendor/gitignore/Clojure.gitignore | 14 +--- vendor/gitignore/CodeIgniter.gitignore | 5 ++ vendor/gitignore/CommonLisp.gitignore | 14 ++++ vendor/gitignore/Coq.gitignore | 29 ++++++- vendor/gitignore/Dart.gitignore | 10 ++- vendor/gitignore/Elisp.gitignore | 6 ++ vendor/gitignore/Elixir.gitignore | 1 + vendor/gitignore/Global/Emacs.gitignore | 5 +- vendor/gitignore/Global/IPythonNotebook.gitignore | 2 - vendor/gitignore/Global/JetBrains.gitignore | 1 + vendor/gitignore/Global/SublimeText.gitignore | 3 + vendor/gitignore/Global/Vim.gitignore | 6 +- vendor/gitignore/Global/VisualStudioCode.gitignore | 1 + vendor/gitignore/Global/Windows.gitignore | 3 +- vendor/gitignore/Go.gitignore | 2 +- vendor/gitignore/Java.gitignore | 3 + vendor/gitignore/Laravel.gitignore | 2 +- vendor/gitignore/Maven.gitignore | 3 + vendor/gitignore/Node.gitignore | 4 + vendor/gitignore/Perl.gitignore | 38 ++++++--- vendor/gitignore/Python.gitignore | 1 + vendor/gitignore/Symfony.gitignore | 5 +- vendor/gitignore/TeX.gitignore | 22 +++++- vendor/gitignore/VisualStudio.gitignore | 5 +- vendor/gitlab-ci-yml/Docker.gitlab-ci.yml | 5 +- vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml | 6 +- vendor/gitlab-ci-yml/Go.gitlab-ci.yml | 37 +++++++++ vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml | 2 +- vendor/gitlab-ci-yml/Openshift.gitlab-ci.yml | 92 ++++++++++++++++++++++ .../autodeploy/OpenShift.gitlab-ci.yml | 74 ----------------- 33 files changed, 292 insertions(+), 127 deletions(-) mode change 100644 => 120000 vendor/gitignore/Clojure.gitignore delete mode 100644 vendor/gitignore/Global/IPythonNotebook.gitignore create mode 100644 vendor/gitlab-ci-yml/Go.gitlab-ci.yml create mode 100644 vendor/gitlab-ci-yml/Openshift.gitlab-ci.yml delete mode 100644 vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml diff --git a/vendor/gitignore/Android.gitignore b/vendor/gitignore/Android.gitignore index 935ceef0680..d028d1251ad 100644 --- a/vendor/gitignore/Android.gitignore +++ b/vendor/gitignore/Android.gitignore @@ -35,6 +35,8 @@ captures/ # Intellij *.iml .idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml .idea/libraries # Keystore files diff --git a/vendor/gitignore/Autotools.gitignore b/vendor/gitignore/Autotools.gitignore index 1e9158e2a85..e3923f96fce 100644 --- a/vendor/gitignore/Autotools.gitignore +++ b/vendor/gitignore/Autotools.gitignore @@ -1,6 +1,11 @@ # http://www.gnu.org/software/automake Makefile.in +/ar-lib +/mdate-sh +/py-compile +/test-driver +/ylwrap # http://www.gnu.org/software/autoconf @@ -9,10 +14,20 @@ Makefile.in /autoscan-*.log /aclocal.m4 /compile +/config.guess /config.h.in +/config.sub /configure /configure.scan /depcomp /install-sh /missing /stamp-h1 + +# https://www.gnu.org/software/libtool/ + +/ltmain.sh + +# http://www.gnu.org/software/texinfo + +/texinfo.tex diff --git a/vendor/gitignore/CMake.gitignore b/vendor/gitignore/CMake.gitignore index 0cc7e4b5275..27ada0591ec 100644 --- a/vendor/gitignore/CMake.gitignore +++ b/vendor/gitignore/CMake.gitignore @@ -4,4 +4,5 @@ CMakeScripts Makefile cmake_install.cmake install_manifest.txt +compile_commands.json CTestTestfile.cmake diff --git a/vendor/gitignore/Clojure.gitignore b/vendor/gitignore/Clojure.gitignore deleted file mode 100644 index a9fe6fba80d..00000000000 --- a/vendor/gitignore/Clojure.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -pom.xml -pom.xml.asc -*.jar -*.class -/lib/ -/classes/ -/target/ -/checkouts/ -.lein-deps-sum -.lein-repl-history -.lein-plugins/ -.lein-failures -.nrepl-port diff --git a/vendor/gitignore/Clojure.gitignore b/vendor/gitignore/Clojure.gitignore new file mode 120000 index 00000000000..7657a270c45 --- /dev/null +++ b/vendor/gitignore/Clojure.gitignore @@ -0,0 +1 @@ +Leiningen.gitignore \ No newline at end of file diff --git a/vendor/gitignore/CodeIgniter.gitignore b/vendor/gitignore/CodeIgniter.gitignore index 0f77d9e1d17..60571a0c383 100644 --- a/vendor/gitignore/CodeIgniter.gitignore +++ b/vendor/gitignore/CodeIgniter.gitignore @@ -4,3 +4,8 @@ */cache/* !*/cache/index.html !*/cache/.htaccess + +user_guide_src/build/* +user_guide_src/cilexer/build/* +user_guide_src/cilexer/dist/* +user_guide_src/cilexer/pycilexer.egg-info/* diff --git a/vendor/gitignore/CommonLisp.gitignore b/vendor/gitignore/CommonLisp.gitignore index 4806e580b60..e7de127b014 100644 --- a/vendor/gitignore/CommonLisp.gitignore +++ b/vendor/gitignore/CommonLisp.gitignore @@ -1,3 +1,17 @@ *.FASL *.fasl *.lisp-temp +*.dfsl +*.pfsl +*.d64fsl +*.p64fsl +*.lx64fsl +*.lx32fsl +*.dx64fsl +*.dx32fsl +*.fx64fsl +*.fx32fsl +*.sx64fsl +*.sx32fsl +*.wx64fsl +*.wx32fsl diff --git a/vendor/gitignore/Coq.gitignore b/vendor/gitignore/Coq.gitignore index d3083b3a605..f25a61d9964 100644 --- a/vendor/gitignore/Coq.gitignore +++ b/vendor/gitignore/Coq.gitignore @@ -1,3 +1,30 @@ -*.vo +.*.aux +*.a +*.cma +*.cmi +*.cmo +*.cmx +*.cmxa +*.cmxs *.glob +*.ml.d +*.ml4.d +*.mli.d +*.mllib.d +*.mlpack.d +*.native +*.o *.v.d +*.vio +*.vo +.coq-native/ +.csdp.cache +.lia.cache +.nia.cache +.nlia.cache +.nra.cache +csdp.cache +lia.cache +nia.cache +nlia.cache +nra.cache diff --git a/vendor/gitignore/Dart.gitignore b/vendor/gitignore/Dart.gitignore index 7c280441649..4b366585ddc 100644 --- a/vendor/gitignore/Dart.gitignore +++ b/vendor/gitignore/Dart.gitignore @@ -1,13 +1,19 @@ # See https://www.dartlang.org/tools/private-files.html # Files and directories created by pub -.buildlog + +# SDK 1.20 and later (no longer creates packages directories) .packages -.project .pub/ build/ + +# Older SDK versions +# (Include if the minimum SDK version specified in pubsepc.yaml is earlier than 1.20) +.project +.buildlog **/packages/ + # Files created by dart2js # (Most Dart developers will use pub build to compile Dart, use/modify these # rules if you intend to use dart2js directly diff --git a/vendor/gitignore/Elisp.gitignore b/vendor/gitignore/Elisp.gitignore index 9b4291b7fe8..206569dc661 100644 --- a/vendor/gitignore/Elisp.gitignore +++ b/vendor/gitignore/Elisp.gitignore @@ -3,3 +3,9 @@ # Packaging .cask + +# Backup files +*~ + +# Undo-tree save-files +*.~undo-tree diff --git a/vendor/gitignore/Elixir.gitignore b/vendor/gitignore/Elixir.gitignore index 755b605549d..ac67aaf3243 100644 --- a/vendor/gitignore/Elixir.gitignore +++ b/vendor/gitignore/Elixir.gitignore @@ -3,3 +3,4 @@ /deps erl_crash.dump *.ez +*.beam diff --git a/vendor/gitignore/Global/Emacs.gitignore b/vendor/gitignore/Global/Emacs.gitignore index 0c96c9ad060..3ac7904dcd2 100644 --- a/vendor/gitignore/Global/Emacs.gitignore +++ b/vendor/gitignore/Global/Emacs.gitignore @@ -39,4 +39,7 @@ flycheck_*.el /server/ # projectiles files -.projectile \ No newline at end of file +.projectile + +# directory configuration +.dir-locals.el diff --git a/vendor/gitignore/Global/IPythonNotebook.gitignore b/vendor/gitignore/Global/IPythonNotebook.gitignore deleted file mode 100644 index 27c13510bf5..00000000000 --- a/vendor/gitignore/Global/IPythonNotebook.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# Temporary data -.ipynb_checkpoints/ diff --git a/vendor/gitignore/Global/JetBrains.gitignore b/vendor/gitignore/Global/JetBrains.gitignore index 0a254147875..e375c744b6d 100644 --- a/vendor/gitignore/Global/JetBrains.gitignore +++ b/vendor/gitignore/Global/JetBrains.gitignore @@ -6,6 +6,7 @@ .idea/tasks.xml # Sensitive or high-churn files: +.idea/dataSources/ .idea/dataSources.ids .idea/dataSources.xml .idea/dataSources.local.xml diff --git a/vendor/gitignore/Global/SublimeText.gitignore b/vendor/gitignore/Global/SublimeText.gitignore index 69c8c2b29ce..95ff2244c99 100644 --- a/vendor/gitignore/Global/SublimeText.gitignore +++ b/vendor/gitignore/Global/SublimeText.gitignore @@ -20,6 +20,9 @@ Package Control.ca-bundle Package Control.system-ca-bundle Package Control.cache/ Package Control.ca-certs/ +Package Control.merged-ca-bundle +Package Control.user-ca-bundle +oscrypto-ca-bundle.crt bh_unicode_properties.cache # Sublime-github package stores a github token in this file diff --git a/vendor/gitignore/Global/Vim.gitignore b/vendor/gitignore/Global/Vim.gitignore index bdc04a0b529..42e7afc1005 100644 --- a/vendor/gitignore/Global/Vim.gitignore +++ b/vendor/gitignore/Global/Vim.gitignore @@ -1,6 +1,8 @@ # swap -[._]*.s[a-w][a-z] -[._]s[a-w][a-z] +[._]*.s[a-v][a-z] +[._]*.sw[a-p] +[._]s[a-v][a-z] +[._]sw[a-p] # session Session.vim # temporary diff --git a/vendor/gitignore/Global/VisualStudioCode.gitignore b/vendor/gitignore/Global/VisualStudioCode.gitignore index d9960081c98..0511e2b51f0 100644 --- a/vendor/gitignore/Global/VisualStudioCode.gitignore +++ b/vendor/gitignore/Global/VisualStudioCode.gitignore @@ -2,3 +2,4 @@ !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json +!.vscode/extensions.json diff --git a/vendor/gitignore/Global/Windows.gitignore b/vendor/gitignore/Global/Windows.gitignore index a0d31452b0e..ba26afd9653 100644 --- a/vendor/gitignore/Global/Windows.gitignore +++ b/vendor/gitignore/Global/Windows.gitignore @@ -1,6 +1,7 @@ -# Windows image file caches +# Windows thumbnail cache files Thumbs.db ehthumbs.db +ehthumbs_vista.db # Folder config file Desktop.ini diff --git a/vendor/gitignore/Go.gitignore b/vendor/gitignore/Go.gitignore index 397a0ed4acb..5e1047c9d78 100644 --- a/vendor/gitignore/Go.gitignore +++ b/vendor/gitignore/Go.gitignore @@ -26,5 +26,5 @@ _testmain.go # Output of the go coverage tool, specifically when used with LiteIDE *.out -# external packages folder +# External packages folder vendor/ diff --git a/vendor/gitignore/Java.gitignore b/vendor/gitignore/Java.gitignore index 32858aad3c3..e44e0860405 100644 --- a/vendor/gitignore/Java.gitignore +++ b/vendor/gitignore/Java.gitignore @@ -1,5 +1,8 @@ *.class +# BlueJ files +*.ctxt + # Mobile Tools for Java (J2ME) .mtj.tmp/ diff --git a/vendor/gitignore/Laravel.gitignore b/vendor/gitignore/Laravel.gitignore index e7c594fa3e2..a2d1564060b 100644 --- a/vendor/gitignore/Laravel.gitignore +++ b/vendor/gitignore/Laravel.gitignore @@ -6,8 +6,8 @@ bootstrap/compiled.php app/storage/ # Laravel 5 & Lumen specific -bootstrap/cache/ public/storage +storage/*.key .env.*.php .env.php .env diff --git a/vendor/gitignore/Maven.gitignore b/vendor/gitignore/Maven.gitignore index 1cdc9f7fd45..9af45b175ae 100644 --- a/vendor/gitignore/Maven.gitignore +++ b/vendor/gitignore/Maven.gitignore @@ -7,3 +7,6 @@ release.properties dependency-reduced-pom.xml buildNumber.properties .mvn/timing.properties + +# Exclude maven wrapper +!/.mvn/wrapper/maven-wrapper.jar diff --git a/vendor/gitignore/Node.gitignore b/vendor/gitignore/Node.gitignore index bc7fc55724c..9a439fcd988 100644 --- a/vendor/gitignore/Node.gitignore +++ b/vendor/gitignore/Node.gitignore @@ -42,3 +42,7 @@ jspm_packages # Output of 'npm pack' *.tgz + +# Yarn Integrity file +.yarn-integrity + diff --git a/vendor/gitignore/Perl.gitignore b/vendor/gitignore/Perl.gitignore index ae2ad536abb..d41364ab18e 100644 --- a/vendor/gitignore/Perl.gitignore +++ b/vendor/gitignore/Perl.gitignore @@ -1,20 +1,34 @@ -/blib/ -/.build/ -_build/ -cover_db/ -inc/ -Build !Build/ -Build.bat .last_cover_stats -/Makefile -/Makefile.old -/MANIFEST.bak /META.yml /META.json /MYMETA.* -nytprof.out -/pm_to_blib *.o *.bs + +# Devel::Cover +cover_db/ + +# Devel::NYTProf +nytprof.out + +# Dizt::Zilla +/.build/ + +# Module::Build +_build/ +Build +Build.bat + +# Module::Install +inc/ + +# ExtUitls::MakeMaker +/blib/ /_eumm/ +/*.gz +/Makefile +/Makefile.old +/MANIFEST.bak +/pm_to_blib +/*.zip diff --git a/vendor/gitignore/Python.gitignore b/vendor/gitignore/Python.gitignore index 6a2bf47ade9..9a05e2debe5 100644 --- a/vendor/gitignore/Python.gitignore +++ b/vendor/gitignore/Python.gitignore @@ -20,6 +20,7 @@ lib64/ parts/ sdist/ var/ +wheels/ *.egg-info/ .installed.cfg *.egg diff --git a/vendor/gitignore/Symfony.gitignore b/vendor/gitignore/Symfony.gitignore index 7d56f982f81..ed4d3c6c28d 100644 --- a/vendor/gitignore/Symfony.gitignore +++ b/vendor/gitignore/Symfony.gitignore @@ -31,9 +31,6 @@ /web/bundles/ /web/uploads/ -# Assets managed by Bower -/web/assets/vendor/ - # PHPUnit /app/phpunit.xml /phpunit.xml @@ -45,4 +42,4 @@ /composer.phar # Backup entities generated with doctrine:generate:entities command -*/Entity/*~ +**/Entity/*~ diff --git a/vendor/gitignore/TeX.gitignore b/vendor/gitignore/TeX.gitignore index 1afbaf197f4..69bfb1eec3e 100644 --- a/vendor/gitignore/TeX.gitignore +++ b/vendor/gitignore/TeX.gitignore @@ -52,12 +52,22 @@ acs-*.bib # beamer *.nav +*.pre *.snm *.vrb +# changes +*.soc + # cprotect *.cpt +# elsarticle (documentclass of Elsevier journals) +*.spl + +# endnotes +*.ent + # fixme *.lox @@ -123,9 +133,7 @@ acs-*.bib *.maf *.mlf *.mlt -*.mtc -*.mtc[0-9] -*.mtc[1-9][0-9] +*.mtc[0-9]* # minted _minted* @@ -140,6 +148,9 @@ _minted* # nomencl *.nlo +# pax +*.pax + # sagetex *.sagetex.sage *.sagetex.py @@ -202,5 +213,8 @@ TSWLatexianTemp* # KBibTeX *~[0-9]* -# auto folder when using emacs and auctex +# auto folder when using emacs and auctex /auto/* + +# expex forward references with \gathertags +*-tags.tex diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore index 09e407344ca..d9e876cfcdd 100644 --- a/vendor/gitignore/VisualStudio.gitignore +++ b/vendor/gitignore/VisualStudio.gitignore @@ -8,7 +8,6 @@ *.user *.userosscache *.sln.docstates -*.vcxproj.filters # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs @@ -43,11 +42,11 @@ TestResult.xml [Rr]eleasePS/ dlldata.c -# DNX +# .NET Core project.lock.json project.fragment.lock.json artifacts/ -Properties/launchSettings.json +**/Properties/launchSettings.json *_i.c *_p.c diff --git a/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml b/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml index f3fa3949656..8c590579934 100644 --- a/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml @@ -7,6 +7,7 @@ services: build: stage: build script: + - export IMAGE_TAG=$(echo -en $CI_BUILD_REF_NAME | tr -c '[:alnum:]_.-' '-') - docker login -u "gitlab-ci-token" -p "$CI_BUILD_TOKEN" $CI_REGISTRY - - docker build --pull -t "$CI_REGISTRY_IMAGE:$CI_BUILD_REF_NAME" . - - docker push "$CI_REGISTRY_IMAGE:$CI_BUILD_REF_NAME" + - docker build --pull -t "$CI_REGISTRY_IMAGE:$IMAGE_TAG" . + - docker push "$CI_REGISTRY_IMAGE:$IMAGE_TAG" diff --git a/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml b/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml index 00f9541e89b..981a77497e2 100644 --- a/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml @@ -1,6 +1,4 @@ -# This template uses the non default language docker image -# The image already has Hex installed. You might want to consider to use `elixir:latest` -image: trenpixster/elixir:latest +image: elixir:latest # Pick zero or more services to be used on all builds. # Only needed when using a docker container to run your tests in. @@ -11,6 +9,8 @@ services: - postgres:latest before_script: + - mix local.rebar --force + - mix local.hex --force - mix deps.get mix: diff --git a/vendor/gitlab-ci-yml/Go.gitlab-ci.yml b/vendor/gitlab-ci-yml/Go.gitlab-ci.yml new file mode 100644 index 00000000000..e23b6e212f0 --- /dev/null +++ b/vendor/gitlab-ci-yml/Go.gitlab-ci.yml @@ -0,0 +1,37 @@ +image: golang:latest + +# The problem is that to be able to use go get, one needs to put +# the repository in the $GOPATH. So for example if your gitlab domain +# is mydomainperso.com, and that your repository is repos/projectname, and +# the default GOPATH being /go, then you'd need to have your +# repository in /go/src/mydomainperso.com/repos/projectname +# Thus, making a symbolic link corrects this. +before_script: + - ln -s /builds /go/src/mydomainperso.com + - cd /go/src/mydomainperso.com/repos/projectname + +stages: + - test + - build + +format: + stage: test + script: + # Add here all the dependencies, or use glide/govendor to get + # them automatically. + # - curl https://glide.sh/get | sh + - go get github.com/alecthomas/kingpin + - go tool vet -composites=false -shadow=true *.go + - go test -race $(go list ./... | grep -v /vendor/) + +compile: + stage: build + script: + # Add here all the dependencies, or use glide/govendor/... + # to get them automatically. + - go get github.com/alecthomas/kingpin + # Better put this in a Makefile + - go build -race -ldflags "-extldflags '-static'" -o mybinary + artifacts: + paths: + - mybinary diff --git a/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml b/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml index 263c4c19999..98d3039ad06 100644 --- a/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml @@ -31,4 +31,4 @@ build: test: stage: test script: - - ./gradlew -g /cache./gradle check + - ./gradlew -g /cache/.gradle check diff --git a/vendor/gitlab-ci-yml/Openshift.gitlab-ci.yml b/vendor/gitlab-ci-yml/Openshift.gitlab-ci.yml new file mode 100644 index 00000000000..2ba5cad9682 --- /dev/null +++ b/vendor/gitlab-ci-yml/Openshift.gitlab-ci.yml @@ -0,0 +1,92 @@ +# This file is a template, and might need editing before it works on your project. +image: ayufan/openshift-cli + +stages: + - test + - review + - staging + - production + +variables: + OPENSHIFT_SERVER: openshift.default.svc.cluster.local + # OPENSHIFT_DOMAIN: apps.example.com + # Configure this variable in Secure Variables: + # OPENSHIFT_TOKEN: my.openshift.token + +test1: + stage: test + before_script: [] + script: + - echo run tests + +test2: + stage: test + before_script: [] + script: + - echo run tests + +.deploy: &deploy + before_script: + - oc login "$OPENSHIFT_SERVER" --token="$OPENSHIFT_TOKEN" --insecure-skip-tls-verify + - oc project "$CI_PROJECT_NAME" 2> /dev/null || oc new-project "$CI_PROJECT_NAME" + script: + - "oc get services $APP 2> /dev/null || oc new-app . --name=$APP --strategy=docker" + - "oc start-build $APP --from-dir=. --follow || sleep 3s || oc start-build $APP --from-dir=. --follow" + - "oc get routes $APP 2> /dev/null || oc expose service $APP --hostname=$APP_HOST" + +review: + <<: *deploy + stage: review + variables: + APP: $CI_BUILD_REF_NAME + APP_HOST: $CI_PROJECT_NAME-$CI_BUILD_REF_NAME.$OPENSHIFT_DOMAIN + environment: + name: review/$CI_BUILD_REF_NAME + url: http://$CI_PROJECT_NAME-$CI_BUILD_REF_NAME.$OPENSHIFT_DOMAIN + on_stop: stop-review + only: + - branches + except: + - master + +stop-review: + <<: *deploy + stage: review + script: + - oc delete all -l "app=$APP" + when: manual + variables: + APP: $CI_BUILD_REF_NAME + GIT_STRATEGY: none + environment: + name: review/$CI_BUILD_REF_NAME + action: stop + only: + - branches + except: + - master + +staging: + <<: *deploy + stage: staging + variables: + APP: staging + APP_HOST: $CI_PROJECT_NAME-staging.$OPENSHIFT_DOMAIN + environment: + name: staging + url: http://$CI_PROJECT_NAME-staging.$OPENSHIFT_DOMAIN + only: + - master + +production: + <<: *deploy + stage: production + variables: + APP: production + APP_HOST: $CI_PROJECT_NAME.$OPENSHIFT_DOMAIN + when: manual + environment: + name: production + url: http://$CI_PROJECT_NAME.$OPENSHIFT_DOMAIN + only: + - master diff --git a/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml b/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml deleted file mode 100644 index e384b585ae0..00000000000 --- a/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml +++ /dev/null @@ -1,74 +0,0 @@ -image: registry.gitlab.com/gitlab-examples/openshift-deploy - -variables: - # Application deployment domain - KUBE_DOMAIN: domain.example.com - -stages: - - build - - test - - review - - staging - - production - -build: - stage: build - script: - - command build - only: - - branches - -production: - stage: production - variables: - CI_ENVIRONMENT_URL: http://production.$KUBE_DOMAIN - script: - - command deploy - environment: - name: production - url: http://production.$KUBE_DOMAIN - when: manual - only: - - master - -staging: - stage: staging - variables: - CI_ENVIRONMENT_URL: http://staging.$KUBE_DOMAIN - script: - - command deploy - environment: - name: staging - url: http://staging.$KUBE_DOMAIN - only: - - master - -review: - stage: review - variables: - CI_ENVIRONMENT_URL: http://$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN - script: - - command deploy - environment: - name: review/$CI_BUILD_REF_NAME - url: http://$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN - on_stop: stop_review - only: - - branches - except: - - master - -stop_review: - stage: review - variables: - GIT_STRATEGY: none - script: - - command destroy - environment: - name: review/$CI_BUILD_REF_NAME - action: stop - when: manual - only: - - branches - except: - - master -- cgit v1.2.1 From b74677489a915570a182e624fc2c7ad537c8e75e Mon Sep 17 00:00:00 2001 From: Toon Claes Date: Fri, 6 Jan 2017 15:51:06 +0100 Subject: Treat environments matching `production/*` as Production For Cycle Analytics, the environment `production` is treated as a special case, and used to update the `first_deployed_to_production_at` metric. With the Review Apps changes, slash syntax is used to group related environments in the list. This is mostly used for `review/` environments, but it's possible for people to have multiple production environments, and someone might then want to group them like `production/eu`, `production/us`, or even just make it look prettier with `production/www.example.com`. In any of these cases, the first deploy to one of these `production/*` environments now is treated as a Production deploy. --- app/models/environment.rb | 2 +- .../24139-production-wildcard-for-cycle-analytics.yml | 4 ++++ doc/user/project/cycle_analytics.md | 15 +++++++++------ spec/models/environment_spec.rb | 17 +++++++++++++++++ 4 files changed, 31 insertions(+), 7 deletions(-) create mode 100644 changelogs/unreleased/24139-production-wildcard-for-cycle-analytics.yml diff --git a/app/models/environment.rb b/app/models/environment.rb index 5cde94b3509..652abf18a8a 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -87,7 +87,7 @@ class Environment < ActiveRecord::Base end def update_merge_request_metrics? - self.name == "production" + (environment_type || name) == "production" end def first_deployment_for(commit) diff --git a/changelogs/unreleased/24139-production-wildcard-for-cycle-analytics.yml b/changelogs/unreleased/24139-production-wildcard-for-cycle-analytics.yml new file mode 100644 index 00000000000..83cf3670ec0 --- /dev/null +++ b/changelogs/unreleased/24139-production-wildcard-for-cycle-analytics.yml @@ -0,0 +1,4 @@ +--- +title: Treat environments matching `production/*` as Production +merge_request: 8500 +author: diff --git a/doc/user/project/cycle_analytics.md b/doc/user/project/cycle_analytics.md index 86fe52ef4ff..62afd8cf247 100644 --- a/doc/user/project/cycle_analytics.md +++ b/doc/user/project/cycle_analytics.md @@ -50,7 +50,7 @@ exception of the staging and production stages, where only data deployed to production are measured. Specifically, if your CI is not set up and you have not defined a `production` -[environment], then you will not have any data for those stages. +or `production/*` [environment], then you will not have any data for those stages. Below you can see in more detail what the various stages of Cycle Analytics mean. @@ -61,7 +61,7 @@ Below you can see in more detail what the various stages of Cycle Analytics mean | Code | Measures the median time between pushing a first commit (previous stage) and creating a merge request (MR) related to that commit. The key to keep the process tracked is to include the [issue closing pattern] to the description of the merge request (for example, `Closes #xxx`, where `xxx` is the number of the issue related to this merge request). If the issue closing pattern is not present in the merge request description, the MR is not considered to the measurement time of the stage. | | Test | Measures the median time to run the entire pipeline for that project. It's related to the time GitLab CI takes to run every job for the commits pushed to that merge request defined in the previous stage. It is basically the start->finish time for all pipelines. `master` is not excluded. It does not attempt to track time for any particular stages. | | Review | Measures the median time taken to review the merge request, between its creation and until it's merged. | -| Staging | Measures the median time between merging the merge request until the very first deployment to production. It's tracked by the [environment] set to `production` (case-sensitive, `Production` won't work) in your GitLab CI configuration. If there isn't a `production` environment, this is not tracked. | +| Staging | Measures the median time between merging the merge request until the very first deployment to production. It's tracked by the [environment] set to `production` or matching `production/*` (case-sensitive, `Production` won't work) in your GitLab CI configuration. If there isn't a production environment, this is not tracked. | | Production| The sum of all time (medians) taken to run the entire process, from issue creation to deploying the code to production. | --- @@ -79,10 +79,13 @@ Here's a little explanation of how this works behind the scenes: etc. To sum up, anything that doesn't follow the [GitLab flow] won't be tracked at all. -So, if a merge request doesn't close an issue or an issue is not labeled with a -label present in the Issue Board or assigned a milestone or a project has no -`production` environment (for staging and production stages), the Cycle Analytics -dashboard won't present any data at all. +So, the Cycle Analytics dashboard won't present any data: +- For merge requests that do not close an issue. +- For issues not labeled with a label present in the Issue Board. +- For issues not assigned a milestone. +- For staging and production stages, if the project has no `production` or `production/*` + environment. + ## Example workflow diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 93eb402e060..96efe1696c3 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -63,6 +63,23 @@ describe Environment, models: true do end end + describe '#update_merge_request_metrics?' do + { 'production' => true, + 'production/eu' => true, + 'production/www.gitlab.com' => true, + 'productioneu' => false, + 'Production' => false, + 'Production/eu' => false, + 'test-production' => false + }.each do |name, expected_value| + it "returns #{expected_value} for #{name}" do + env = create(:environment, name: name) + + expect(env.update_merge_request_metrics?).to eq(expected_value) + end + end + end + describe '#first_deployment_for' do let(:project) { create(:project) } let!(:deployment) { create(:deployment, environment: environment, ref: commit.parent.id) } -- cgit v1.2.1 From a8177e03e5eeade5b8af405dbb4bcd8a6d06bb0a Mon Sep 17 00:00:00 2001 From: Adam Niedzielski Date: Mon, 9 Jan 2017 18:19:48 +0100 Subject: Introduce "stub_env" test helper for safely stubbing environment variables --- spec/db/production/settings.rb | 5 +++-- spec/initializers/secret_token_spec.rb | 7 ++++--- spec/support/stub_env.rb | 7 +++++++ 3 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 spec/support/stub_env.rb diff --git a/spec/db/production/settings.rb b/spec/db/production/settings.rb index a7c5283df94..007b35bbb77 100644 --- a/spec/db/production/settings.rb +++ b/spec/db/production/settings.rb @@ -2,10 +2,11 @@ require 'spec_helper' require 'rainbow/ext/string' describe 'seed production settings', lib: true do + include StubENV + context 'GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN is set in the environment' do before do - allow(ENV).to receive(:[]).and_call_original - allow(ENV).to receive(:[]).with('GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN').and_return('013456789') + stub_env('GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN', '013456789') end it 'writes the token to the database' do diff --git a/spec/initializers/secret_token_spec.rb b/spec/initializers/secret_token_spec.rb index 837b0de9a4c..ad7f032d1e5 100644 --- a/spec/initializers/secret_token_spec.rb +++ b/spec/initializers/secret_token_spec.rb @@ -2,10 +2,11 @@ require 'spec_helper' require_relative '../../config/initializers/secret_token' describe 'create_tokens', lib: true do + include StubENV + let(:secrets) { ActiveSupport::OrderedOptions.new } before do - allow(ENV).to receive(:[]).and_call_original allow(File).to receive(:write) allow(File).to receive(:delete) allow(Rails).to receive_message_chain(:application, :secrets).and_return(secrets) @@ -17,7 +18,7 @@ describe 'create_tokens', lib: true do context 'setting secret_key_base and otp_key_base' do context 'when none of the secrets exist' do before do - allow(ENV).to receive(:[]).with('SECRET_KEY_BASE').and_return(nil) + stub_env('SECRET_KEY_BASE', nil) allow(File).to receive(:exist?).with('.secret').and_return(false) allow(File).to receive(:exist?).with('config/secrets.yml').and_return(false) allow(self).to receive(:warn_missing_secret) @@ -69,7 +70,7 @@ describe 'create_tokens', lib: true do context 'when secret_key_base exists in the environment and secrets.yml' do before do - allow(ENV).to receive(:[]).with('SECRET_KEY_BASE').and_return('env_key') + stub_env('SECRET_KEY_BASE', 'env_key') secrets.secret_key_base = 'secret_key_base' secrets.otp_key_base = 'otp_key_base' end diff --git a/spec/support/stub_env.rb b/spec/support/stub_env.rb new file mode 100644 index 00000000000..18597b5c71f --- /dev/null +++ b/spec/support/stub_env.rb @@ -0,0 +1,7 @@ +module StubENV + def stub_env(key, value) + allow(ENV).to receive(:[]).and_call_original unless @env_already_stubbed + @env_already_stubbed ||= true + allow(ENV).to receive(:[]).with(key).and_return(value) + end +end -- cgit v1.2.1 From 82692ea2140fc35cca4157050c29abffbc5d71b0 Mon Sep 17 00:00:00 2001 From: Adam Niedzielski Date: Mon, 9 Jan 2017 18:38:53 +0100 Subject: Restore backup correctly when "BACKUP" environment variable is passed --- .../restore-backup-when-env-variable-is-passed.yml | 4 + doc/raketasks/backup_restore.md | 8 +- lib/backup/manager.rb | 44 +++++--- spec/lib/gitlab/backup/manager_spec.rb | 114 ++++++++++++++++++--- spec/tasks/gitlab/backup_rake_spec.rb | 2 +- 5 files changed, 136 insertions(+), 36 deletions(-) create mode 100644 changelogs/unreleased/restore-backup-when-env-variable-is-passed.yml diff --git a/changelogs/unreleased/restore-backup-when-env-variable-is-passed.yml b/changelogs/unreleased/restore-backup-when-env-variable-is-passed.yml new file mode 100644 index 00000000000..8ec3cfdbb08 --- /dev/null +++ b/changelogs/unreleased/restore-backup-when-env-variable-is-passed.yml @@ -0,0 +1,4 @@ +--- +title: Restore backup correctly when "BACKUP" environment variable is passed +merge_request: 8477 +author: diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 43ddc419054..51f7454610d 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -9,6 +9,9 @@ This archive will be saved in `backup_path`, which is specified in the The filename will be `[TIMESTAMP]_gitlab_backup.tar`, where `TIMESTAMP` identifies the time at which each backup was created. +> In GitLab 8.15 we changed the timestamp format from `EPOCH` (`1393513186`) +> to `EPOCH_YYYY_MM_DD` (`1393513186_2014_02_27`) + You can only restore a backup to exactly the same version of GitLab on which it was created. The best way to migrate your repositories from one server to another is through backup restore. @@ -223,7 +226,8 @@ For installations from source: ## Backup archive permissions -The backup archives created by GitLab (123456_gitlab_backup.tar) will have owner/group git:git and 0600 permissions by default. +The backup archives created by GitLab (`1393513186_2014_02_27_gitlab_backup.tar`) +will have owner/group git:git and 0600 permissions by default. This is meant to avoid other system users reading GitLab's data. If you need the backup archives to have different permissions you can use the 'archive_permissions' setting. @@ -335,7 +339,7 @@ First make sure your backup tar file is in the backup directory described in the `/var/opt/gitlab/backups`. ```shell -sudo cp 1393513186_gitlab_backup.tar /var/opt/gitlab/backups/ +sudo cp 1393513186_2014_02_27_gitlab_backup.tar /var/opt/gitlab/backups/ ``` Stop the processes that are connected to the database. Leave the rest of GitLab diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index 7e6537e3d9e..cefbfdce3bb 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -2,6 +2,7 @@ module Backup class Manager ARCHIVES_TO_BACKUP = %w[uploads builds artifacts lfs registry] FOLDERS_TO_BACKUP = %w[repositories db] + FILE_NAME_SUFFIX = '_gitlab_backup.tar' def pack # Make sure there is a connection @@ -14,7 +15,7 @@ module Backup s[:gitlab_version] = Gitlab::VERSION s[:tar_version] = tar_version s[:skipped] = ENV["SKIP"] - tar_file = s[:backup_created_at].strftime('%s_%Y_%m_%d') + '_gitlab_backup.tar' + tar_file = "#{s[:backup_created_at].strftime('%s_%Y_%m_%d')}#{FILE_NAME_SUFFIX}" Dir.chdir(Gitlab.config.backup.path) do File.open("#{Gitlab.config.backup.path}/backup_information.yml", @@ -82,7 +83,7 @@ module Backup removed = 0 Dir.chdir(Gitlab.config.backup.path) do - Dir.glob('*_gitlab_backup.tar').each do |file| + Dir.glob("*#{FILE_NAME_SUFFIX}").each do |file| next unless file =~ /(\d+)(?:_\d{4}_\d{2}_\d{2})?_gitlab_backup\.tar/ timestamp = $1.to_i @@ -108,41 +109,50 @@ module Backup Dir.chdir(Gitlab.config.backup.path) # check for existing backups in the backup dir - file_list = Dir.glob("*_gitlab_backup.tar") - puts "no backups found" if file_list.count == 0 + file_list = Dir.glob("*#{FILE_NAME_SUFFIX}") + + if file_list.count == 0 + $progress.puts "No backups found in #{Gitlab.config.backup.path}" + $progress.puts "Please make sure that file name ends with #{FILE_NAME_SUFFIX}" + exit 1 + end if file_list.count > 1 && ENV["BACKUP"].nil? - puts "Found more than one backup, please specify which one you want to restore:" - puts "rake gitlab:backup:restore BACKUP=timestamp_of_backup" + $progress.puts 'Found more than one backup, please specify which one you want to restore:' + $progress.puts 'rake gitlab:backup:restore BACKUP=timestamp_of_backup' exit 1 end - tar_file = ENV["BACKUP"].nil? ? file_list.first : file_list.grep(ENV['BACKUP']).first + if ENV['BACKUP'].present? + tar_file = "#{ENV['BACKUP']}#{FILE_NAME_SUFFIX}" + else + tar_file = file_list.first + end unless File.exist?(tar_file) - puts "The specified backup doesn't exist!" + $progress.puts "The backup file #{tar_file} does not exist!" exit 1 end - $progress.print "Unpacking backup ... " + $progress.print 'Unpacking backup ... ' unless Kernel.system(*%W(tar -xf #{tar_file})) - puts "unpacking backup failed".color(:red) + $progress.puts 'unpacking backup failed'.color(:red) exit 1 else - $progress.puts "done".color(:green) + $progress.puts 'done'.color(:green) end ENV["VERSION"] = "#{settings[:db_version]}" if settings[:db_version].to_i > 0 # restoring mismatching backups can lead to unexpected problems if settings[:gitlab_version] != Gitlab::VERSION - puts "GitLab version mismatch:".color(:red) - puts " Your current GitLab version (#{Gitlab::VERSION}) differs from the GitLab version in the backup!".color(:red) - puts " Please switch to the following version and try again:".color(:red) - puts " version: #{settings[:gitlab_version]}".color(:red) - puts - puts "Hint: git checkout v#{settings[:gitlab_version]}" + $progress.puts 'GitLab version mismatch:'.color(:red) + $progress.puts " Your current GitLab version (#{Gitlab::VERSION}) differs from the GitLab version in the backup!".color(:red) + $progress.puts ' Please switch to the following version and try again:'.color(:red) + $progress.puts " version: #{settings[:gitlab_version]}".color(:red) + $progress.puts + $progress.puts "Hint: git checkout v#{settings[:gitlab_version]}" exit 1 end end diff --git a/spec/lib/gitlab/backup/manager_spec.rb b/spec/lib/gitlab/backup/manager_spec.rb index 1b749d1bd39..f84782ab440 100644 --- a/spec/lib/gitlab/backup/manager_spec.rb +++ b/spec/lib/gitlab/backup/manager_spec.rb @@ -1,9 +1,27 @@ require 'spec_helper' describe Backup::Manager, lib: true do - describe '#remove_old' do - let(:progress) { StringIO.new } + include StubENV + + let(:progress) { StringIO.new } + + before do + allow(progress).to receive(:puts) + allow(progress).to receive(:print) + + allow_any_instance_of(String).to receive(:color) do |string, _color| + string + end + + @old_progress = $progress # rubocop:disable Style/GlobalVars + $progress = progress # rubocop:disable Style/GlobalVars + end + + after do + $progress = @old_progress # rubocop:disable Style/GlobalVars + end + describe '#remove_old' do let(:files) do [ '1451606400_2016_01_01_gitlab_backup.tar', @@ -20,20 +38,6 @@ describe Backup::Manager, lib: true do allow(Dir).to receive(:glob).and_return(files) allow(FileUtils).to receive(:rm) allow(Time).to receive(:now).and_return(Time.utc(2016)) - - allow(progress).to receive(:puts) - allow(progress).to receive(:print) - - allow_any_instance_of(String).to receive(:color) do |string, _color| - string - end - - @old_progress = $progress # rubocop:disable Style/GlobalVars - $progress = progress # rubocop:disable Style/GlobalVars - end - - after do - $progress = @old_progress # rubocop:disable Style/GlobalVars end context 'when keep_time is zero' do @@ -124,4 +128,82 @@ describe Backup::Manager, lib: true do end end end + + describe '#unpack' do + before do + allow(Dir).to receive(:chdir) + end + + context 'when there are no backup files in the directory' do + before do + allow(Dir).to receive(:glob).and_return([]) + end + + it 'fails the operation and prints an error' do + expect { subject.unpack }.to raise_error SystemExit + expect(progress).to have_received(:puts) + .with(a_string_matching('No backups found')) + end + end + + context 'when there are two backup files in the directory and BACKUP variable is not set' do + before do + allow(Dir).to receive(:glob).and_return( + [ + '1451606400_2016_01_01_gitlab_backup.tar', + '1451520000_2015_12_31_gitlab_backup.tar', + ] + ) + end + + it 'fails the operation and prints an error' do + expect { subject.unpack }.to raise_error SystemExit + expect(progress).to have_received(:puts) + .with(a_string_matching('Found more than one backup')) + end + end + + context 'when BACKUP variable is set to a non-existing file' do + before do + allow(Dir).to receive(:glob).and_return( + [ + '1451606400_2016_01_01_gitlab_backup.tar' + ] + ) + allow(File).to receive(:exist?).and_return(false) + + stub_env('BACKUP', 'wrong') + end + + it 'fails the operation and prints an error' do + expect { subject.unpack }.to raise_error SystemExit + expect(File).to have_received(:exist?).with('wrong_gitlab_backup.tar') + expect(progress).to have_received(:puts) + .with(a_string_matching('The backup file wrong_gitlab_backup.tar does not exist')) + end + end + + context 'when BACKUP variable is set to a correct file' do + before do + allow(Dir).to receive(:glob).and_return( + [ + '1451606400_2016_01_01_gitlab_backup.tar' + ] + ) + allow(File).to receive(:exist?).and_return(true) + allow(Kernel).to receive(:system).and_return(true) + allow(YAML).to receive(:load_file).and_return(gitlab_version: Gitlab::VERSION) + + stub_env('BACKUP', '1451606400_2016_01_01') + end + + it 'unpacks the file' do + subject.unpack + + expect(Kernel).to have_received(:system) + .with("tar", "-xf", "1451606400_2016_01_01_gitlab_backup.tar") + expect(progress).to have_received(:puts).with(a_string_matching('done')) + end + end + end end diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index a9fea5f1e81..bc751d20ce1 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -41,7 +41,7 @@ describe 'gitlab:app namespace rake task' do context 'gitlab version' do before do - allow(Dir).to receive(:glob).and_return([]) + allow(Dir).to receive(:glob).and_return(['1_gitlab_backup.tar']) allow(Dir).to receive(:chdir) allow(File).to receive(:exist?).and_return(true) allow(Kernel).to receive(:system).and_return(true) -- cgit v1.2.1 From 2955c8cc8b76666404569f00e49457262ffbc895 Mon Sep 17 00:00:00 2001 From: Ryan Harris Date: Mon, 9 Jan 2017 12:50:04 -0500 Subject: Display project avatars on Admin Area and Projects pages for mobile views --- app/assets/stylesheets/framework/mobile.scss | 4 ---- changelogs/unreleased/26435-show-project-avatars-on-mobile.yml | 4 ++++ 2 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/26435-show-project-avatars-on-mobile.yml diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 7eb9962ba33..92d687996e9 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -37,10 +37,6 @@ display: none; } - .project-avatar { - display: none; - } - .project-home-panel { padding-left: 0 !important; diff --git a/changelogs/unreleased/26435-show-project-avatars-on-mobile.yml b/changelogs/unreleased/26435-show-project-avatars-on-mobile.yml new file mode 100644 index 00000000000..43afdf45013 --- /dev/null +++ b/changelogs/unreleased/26435-show-project-avatars-on-mobile.yml @@ -0,0 +1,4 @@ +--- +title: Display project avatars on Admin Area and Projects pages for mobile views +merge_request: +author: Ryan Harris -- cgit v1.2.1 From e98ad707fd61b9f353eff24c2a388c702d8088ca Mon Sep 17 00:00:00 2001 From: Ken Ding Date: Tue, 10 Jan 2017 05:03:39 +1030 Subject: squash 3 commits --- app/views/projects/branches/index.html.haml | 3 ++- changelogs/unreleased/26129-add-link-to-branches-page.yml | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/26129-add-link-to-branches-page.yml diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index ecd812312c0..5f8f56150f9 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -5,7 +5,8 @@ %div{ class: container_class } .top-area.adjust .nav-text - Protected branches can be managed in project settings + Protected branches can be managed in + = link_to 'project settings', namespace_project_protected_branches_path(@project.namespace, @project) .nav-controls = form_tag(filter_branches_path, method: :get) do diff --git a/changelogs/unreleased/26129-add-link-to-branches-page.yml b/changelogs/unreleased/26129-add-link-to-branches-page.yml new file mode 100644 index 00000000000..aceb92dbb9c --- /dev/null +++ b/changelogs/unreleased/26129-add-link-to-branches-page.yml @@ -0,0 +1,4 @@ +--- +title: Convert project setting text into protected branch path link +merge_request: 8377 +author: Ken Ding -- cgit v1.2.1 From e797f7100d32c5a3089f0c3c934b9bd8d1615e9b Mon Sep 17 00:00:00 2001 From: Ryan Harris Date: Mon, 9 Jan 2017 15:20:42 -0500 Subject: Adjusted widths on Admin Area projects page to prevent misaligned floats --- app/assets/stylesheets/pages/projects.scss | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index e30d73886e1..9455ba3b98a 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -587,11 +587,21 @@ pre.light-well { .project-full-name { @include str-truncated; + + @media (max-width: $screen-xs-max) { + max-width: 50%; + } } .controls { line-height: $list-text-height; + .badge { + @media (max-width: $screen-xs-max) { + display: none; + } + } + a:hover { text-decoration: none; } @@ -605,6 +615,12 @@ pre.light-well { top: 2px; } } + + .description p { + @media (max-width: $screen-xs-max) { + max-width: 50%; + } + } } .bottom { -- cgit v1.2.1 From 71dc5af9ce5e25d8d3219b296e23c3ca6340451b Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 4 Nov 2016 16:27:11 -0500 Subject: Add basic search --- app/assets/javascripts/dispatcher.js.es6 | 3 + .../filtered_search/filtered_search_bundle.js | 13 +++ .../filtered_search/filtered_search_manager.js.es6 | 104 +++++++++++++++++++++ app/assets/stylesheets/framework/filters.scss | 24 +++++ app/views/projects/issues/index.html.haml | 6 +- app/views/shared/issuable/_search_bar.html.haml | 76 +++++++++++++++ config/application.rb | 1 + 7 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 app/assets/javascripts/filtered_search/filtered_search_bundle.js create mode 100644 app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 create mode 100644 app/views/shared/issuable/_search_bar.html.haml diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 54f13e328bd..5a9ee5c7d78 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -84,6 +84,9 @@ break; case 'projects:merge_requests:index': case 'projects:issues:index': + if(gl.hasOwnProperty('FilteredSearchManager')) { + new gl.FilteredSearchManager(); + } Issuable.init(); new gl.IssuableBulkActions({ prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_', diff --git a/app/assets/javascripts/filtered_search/filtered_search_bundle.js b/app/assets/javascripts/filtered_search/filtered_search_bundle.js new file mode 100644 index 00000000000..656979ba82f --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_bundle.js @@ -0,0 +1,13 @@ + /* eslint-disable */ + // This is a manifest file that'll be compiled into including all the files listed below. + // Add new JavaScript code in separate files in this directory and they'll automatically + // be included in the compiled file accessible from http://example.com/assets/application.js + // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the + // the compiled file. + // + /*= require_tree . */ + + (function() { + + }).call(this); + \ No newline at end of file diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 new file mode 100644 index 00000000000..797473f2044 --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -0,0 +1,104 @@ +((global) => { + const TOKEN_TYPE_STRING = 'string'; + const TOKEN_TYPE_ARRAY = 'array'; + + const validTokenKeys = [{ + key: 'author', + type: 'string', + },{ + key: 'assignee', + type: 'string' + },{ + key: 'milestone', + type: 'string' + },{ + key: 'label', + type: 'array' + },]; + + class FilteredSearchManager { + constructor() { + this.bindEvents(); + this.clearTokens(); + } + + bindEvents() { + const input = document.querySelector('.filtered-search'); + + input.addEventListener('input', this.tokenize.bind(this)); + input.addEventListener('keydown', this.checkForEnter.bind(this)); + } + + clearTokens() { + this.tokens = []; + this.searchToken = ''; + } + + tokenize(event) { + // Re-calculate tokens + this.clearTokens(); + + // TODO: Current implementation does not support token values that have valid spaces in them + // Example/ label:community contribution + const input = event.target.value; + const inputs = input.split(' '); + let searchTerms = ''; + + inputs.forEach((i) => { + const colonIndex = i.indexOf(':'); + + // Check if text is a token + if (colonIndex !== -1) { + const tokenKey = i.slice(0, colonIndex).toLowerCase(); + const tokenValue = i.slice(colonIndex + 1); + + const match = validTokenKeys.filter((v) => { + return v.name === tokenKey; + })[0]; + + if (match) { + this.tokens.push = { + key: match.key, + value: tokenValue, + }; + } + } else { + searchTerms += i + ' '; + } + }, this); + + this.searchToken = searchTerms.trim(); + this.printTokens(); + } + + printTokens() { + console.log(this.tokens); + console.log(this.searchToken); + } + + checkForEnter(event) { + if (event.key === 'Enter') { + event.stopPropagation(); + event.preventDefault(); + this.search(); + } + } + + search() { + console.log('search'); + let path = '?scope=all&state=opened&utf8=âś“'; + + this.tokens.foreach((token) => { + + }); + + if (this.searchToken) { + path += '&search=' + this.searchToken; + } + + window.location = path; + } + } + + global.FilteredSearchManager = FilteredSearchManager; +})(window.gl || (window.gl = {})); \ No newline at end of file diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 19827943385..a565642ba38 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -23,3 +23,27 @@ } } +.filtered-search-container { + display: flex; +} + +.filtered-search-input-container { + display: flex; + position: relative; + width: 100%; + + .form-control { + padding-left: 25px; + + &:focus ~ .fa-filter { + color: #444; + } + } + + .fa-filter { + position: absolute; + left: 10px; + top: 10px; + color: $gray-darkest; + } +} diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 26f3f0ac292..18e8372ecab 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -6,6 +6,9 @@ = content_for :sub_nav do = render "projects/issues/head" +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('filtered_search/filtered_search_bundle.js') + = content_for :meta_tags do - if current_user = auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{@project.name} issues") @@ -20,7 +23,6 @@ = icon('rss') %span.icon-label Subscribe - = render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project) - if can? current_user, :create_issue, @project = link_to new_namespace_project_issue_path(@project.namespace, @project, @@ -30,7 +32,7 @@ title: "New Issue", id: "new_issue_link" do New Issue - = render 'shared/issuable/filter', type: :issues + = render 'shared/issuable/search_bar', type: :issues .issues-holder = render 'issues' diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml new file mode 100644 index 00000000000..40c1bd3ef98 --- /dev/null +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -0,0 +1,76 @@ +- finder = controller.controller_name == 'issues' || controller.controller_name == 'boards' ? issues_finder : merge_requests_finder +- boards_page = controller.controller_name == 'boards' + +.issues-filters + .issues-details-filters.row-content-block.second-block + = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do + - if params[:search].present? + = hidden_field_tag :search, params[:search] + - if @bulk_edit + .check-all-holder + = check_box_tag "check_all_issues", nil, false, + class: "check_all_issues left" + .issues-other-filters.filtered-search-container + .filtered-search-input-container + %input.form-control.filtered-search{ placeholder: 'Search or filter results...' } + = icon('filter') + .pull-right + - if boards_page + #js-boards-seach.issue-boards-search + %input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" } + - if can?(current_user, :admin_list, @project) + .dropdown.pull-right + %button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } } + Create new list + .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable + = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Create a new list" } + - if can?(current_user, :admin_label, @project) + = render partial: "shared/issuable/label_page_create" + = dropdown_loading + - else + = render 'shared/sort_dropdown' + + - if @bulk_edit + .issues_bulk_update.hide + = form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: 'bulk-update' do + .filter-item.inline + = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do + %ul + %li + %a{href: "#", data: {id: "reopen"}} Open + %li + %a{href: "#", data: {id: "close"}} Closed + .filter-item.inline + = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", + placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } }) + .filter-item.inline + = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) + .filter-item.inline.labels-filter + = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true } + .filter-item.inline + = dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do + %ul + %li + %a{href: "#", data: {id: "subscribe"}} Subscribe + %li + %a{href: "#", data: {id: "unsubscribe"}} Unsubscribe + + = hidden_field_tag 'update[issuable_ids]', [] + = hidden_field_tag :state_event, params[:state_event] + .filter-item.inline + = button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save" + - has_labels = @labels && @labels.any? + .row-content-block.second-block.filtered-labels{ class: ("hidden" unless has_labels) } + - if has_labels + = render 'shared/labels_row', labels: @labels + +:javascript + new UsersSelect(); + new LabelsSelect(); + new MilestoneSelect(); + new IssueStatusSelect(); + new SubscriptionSelect(); + $('form.filter-form').on('submit', function (event) { + event.preventDefault(); + Turbolinks.visit(this.action + '&' + $(this).serialize()); + }); diff --git a/config/application.rb b/config/application.rb index 1de7fb7bdb8..aa52b0cd512 100644 --- a/config/application.rb +++ b/config/application.rb @@ -106,6 +106,7 @@ module Gitlab config.assets.precompile << "blob_edit/blob_edit_bundle.js" config.assets.precompile << "snippet/snippet_bundle.js" config.assets.precompile << "terminal/terminal_bundle.js" + config.assets.precompile << "filtered_search/filtered_search_bundle.js" config.assets.precompile << "lib/utils/*.js" config.assets.precompile << "lib/*.js" config.assets.precompile << "u2f.js" -- cgit v1.2.1 From cf8ae790d13f69d15f3d279565abbba3b065fba4 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 7 Nov 2016 16:18:50 -0600 Subject: Add filter params to search --- .../filtered_search/filtered_search_manager.js.es6 | 36 ++++++++++++++-------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 797473f2044..c26a46a8558 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -5,15 +5,19 @@ const validTokenKeys = [{ key: 'author', type: 'string', + param: 'id', },{ key: 'assignee', - type: 'string' + type: 'string', + param: 'id', },{ key: 'milestone', - type: 'string' + type: 'string', + param: 'title', },{ key: 'label', - type: 'array' + type: 'array', + param: 'name%5B%5D', },]; class FilteredSearchManager { @@ -53,14 +57,14 @@ const tokenValue = i.slice(colonIndex + 1); const match = validTokenKeys.filter((v) => { - return v.name === tokenKey; + return v.key === tokenKey; })[0]; - if (match) { - this.tokens.push = { - key: match.key, - value: tokenValue, - }; + if (match && tokenValue.length > 0) { + this.tokens.push({ + key: match.key, + value: tokenValue, + }); } } else { searchTerms += i + ' '; @@ -72,8 +76,11 @@ } printTokens() { - console.log(this.tokens); - console.log(this.searchToken); + console.log('tokens:') + this.tokens.forEach((token) => { + console.log(token); + }) + console.log('search: ' + this.searchToken); } checkForEnter(event) { @@ -88,8 +95,13 @@ console.log('search'); let path = '?scope=all&state=opened&utf8=âś“'; - this.tokens.foreach((token) => { + this.tokens.forEach((token) => { + const param = validTokenKeys.find((t) => { + return t.key === token.key; + }).param; + + path += `&${token.key}_${param}=${token.value}`; }); if (this.searchToken) { -- cgit v1.2.1 From 9e8f0e63b46ad4540eb2cf8e6206ebc22200f670 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 7 Nov 2016 16:19:15 -0600 Subject: Load searched params into input field --- .../filtered_search/filtered_search_manager.js.es6 | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index c26a46a8558..44718e8306c 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -23,6 +23,7 @@ class FilteredSearchManager { constructor() { this.bindEvents(); + this.loadSearchParamsFromURL(); this.clearTokens(); } @@ -38,6 +39,31 @@ this.searchToken = ''; } + loadSearchParamsFromURL() { + const params = window.location.search.split('&'); + let inputValue = ''; + + params.forEach((p) => { + const split = p.split('='); + const key = split[0]; + const value = split[1]; + + const match = validTokenKeys.find((t) => { + return key === `${t.key}_${t.param}`; + }); + + if (match) { + const sanitizedKey = key.slice(0, key.indexOf('_')); + inputValue += `${sanitizedKey}:${value} `; + } else if (!match && key === 'search') { + inputValue += `${value} `; + } + }); + + // Trim the last space value + document.querySelector('.filtered-search').value = inputValue.trim(); + } + tokenize(event) { // Re-calculate tokens this.clearTokens(); -- cgit v1.2.1 From fc6eab6919e5cc2426328061df22e9c8985f201b Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 7 Nov 2016 16:19:30 -0600 Subject: Remove shared/labels_row --- app/views/shared/issuable/_search_bar.html.haml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 40c1bd3ef98..db9011d5d57 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -59,10 +59,6 @@ = hidden_field_tag :state_event, params[:state_event] .filter-item.inline = button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save" - - has_labels = @labels && @labels.any? - .row-content-block.second-block.filtered-labels{ class: ("hidden" unless has_labels) } - - if has_labels - = render 'shared/labels_row', labels: @labels :javascript new UsersSelect(); -- cgit v1.2.1 From d0165c82877cbc0ddd939713e7365337e0e5478f Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 7 Nov 2016 16:33:51 -0600 Subject: Add author_username and assignee_username --- .../filtered_search/filtered_search_manager.js.es6 | 4 ++-- app/finders/issuable_finder.rb | 24 ++++++++++++++++------ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 44718e8306c..94c0b99a1e1 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -5,11 +5,11 @@ const validTokenKeys = [{ key: 'author', type: 'string', - param: 'id', + param: 'username', },{ key: 'assignee', type: 'string', - param: 'id', + param: 'username', },{ key: 'milestone', type: 'string', diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index b4c14d05eaf..2afde8ece65 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -165,31 +165,43 @@ class IssuableFinder end end - def assignee? + def assignee_id? params[:assignee_id].present? end + def assignee_username? + params[:assignee_username].present? + end + def assignee return @assignee if defined?(@assignee) @assignee = - if assignee? && params[:assignee_id] != NONE + if assignee_id? && params[:assignee_id] != NONE User.find(params[:assignee_id]) + elsif assignee_username? && params[:assignee_username] != NONE + User.find_by(username: params[:assignee_username]) else nil end end - def author? + def author_id? params[:author_id].present? end + def author_username? + params[:author_username].present? + end + def author return @author if defined?(@author) @author = - if author? && params[:author_id] != NONE + if author_id? && params[:author_id] != NONE User.find(params[:author_id]) + elsif author_username? && params[:author_username] != NONE + User.find_by(username: params[:author_username]) else nil end @@ -263,7 +275,7 @@ class IssuableFinder end def by_assignee(items) - if assignee? + if assignee_id? || assignee_username? items = items.where(assignee_id: assignee.try(:id)) end @@ -271,7 +283,7 @@ class IssuableFinder end def by_author(items) - if author? + if author_id? || author_username? items = items.where(author_id: author.try(:id)) end -- cgit v1.2.1 From 339c5d43262d0061a70b0b485f5fe75f49a6cd0b Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 8 Nov 2016 11:35:28 -0600 Subject: Sanitize spaces in search term --- .../javascripts/filtered_search/filtered_search_manager.js.es6 | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 94c0b99a1e1..c9d7a99ae44 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -56,7 +56,9 @@ const sanitizedKey = key.slice(0, key.indexOf('_')); inputValue += `${sanitizedKey}:${value} `; } else if (!match && key === 'search') { - inputValue += `${value} `; + // Sanitize value as URL converts spaces into %20 + const sanitizedValue = value.replace('%20', ' '); + inputValue += `${sanitizedValue} `; } }); @@ -139,4 +141,4 @@ } global.FilteredSearchManager = FilteredSearchManager; -})(window.gl || (window.gl = {})); \ No newline at end of file +})(window.gl || (window.gl = {})); -- cgit v1.2.1 From 823185eca1dd3483f0c178991870c9727aad6470 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 8 Nov 2016 11:36:03 -0600 Subject: Fixed bug where search terms with colons were not searchable --- .../javascripts/filtered_search/filtered_search_manager.js.es6 | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index c9d7a99ae44..7acdabe3ef2 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -76,10 +76,13 @@ const inputs = input.split(' '); let searchTerms = ''; + const addSearchTerm = function addSearchTerm(term) { + searchTerms += term + ' '; + } + inputs.forEach((i) => { const colonIndex = i.indexOf(':'); - // Check if text is a token if (colonIndex !== -1) { const tokenKey = i.slice(0, colonIndex).toLowerCase(); const tokenValue = i.slice(colonIndex + 1); @@ -93,9 +96,11 @@ key: match.key, value: tokenValue, }); + } else { + addSearchTerm(i); } } else { - searchTerms += i + ' '; + addSearchTerm(i); } }, this); -- cgit v1.2.1 From a257f48946d9d002d829e116cc4acb6349240318 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 8 Nov 2016 12:47:53 -0600 Subject: Add clear search button --- .../filtered_search/filtered_search_manager.js.es6 | 21 +++++++++++++++++++++ app/assets/stylesheets/framework/filters.scss | 22 ++++++++++++++++++++-- app/views/shared/issuable/_search_bar.html.haml | 2 ++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 7acdabe3ef2..ad988fe2072 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -29,9 +29,23 @@ bindEvents() { const input = document.querySelector('.filtered-search'); + const clearSearch = document.querySelector('.clear-search'); input.addEventListener('input', this.tokenize.bind(this)); input.addEventListener('keydown', this.checkForEnter.bind(this)); + + clearSearch.addEventListener('click', this.clearSearch.bind(this)); + } + + clearSearch(event) { + event.stopPropagation(); + event.preventDefault(); + + this.clearTokens(); + const input = document.querySelector('.filtered-search'); + input.value = ''; + + event.target.classList.add('hidden'); } clearTokens() { @@ -64,12 +78,19 @@ // Trim the last space value document.querySelector('.filtered-search').value = inputValue.trim(); + + if (inputValue.trim()) { + document.querySelector('.clear-search').classList.remove('hidden'); + } } tokenize(event) { // Re-calculate tokens this.clearTokens(); + // Enable clear button + document.querySelector('.clear-search').classList.remove('hidden'); + // TODO: Current implementation does not support token values that have valid spaces in them // Example/ label:community contribution const input = event.target.value; diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index a565642ba38..b192455f5f0 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -39,11 +39,29 @@ color: #444; } } - .fa-filter { position: absolute; - left: 10px; top: 10px; + left: 10px; + color: $gray-darkest; + } + + .fa-times { + right: 10px; color: $gray-darkest; } + + .clear-search { + width: 35px; + background-color: transparent; + border: none; + position: absolute; + right: 0px; + height: 100%; + outline: none; + + &:hover .fa-times { + color: #444; + } + } } diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index db9011d5d57..5e759301a04 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -14,6 +14,8 @@ .filtered-search-input-container %input.form-control.filtered-search{ placeholder: 'Search or filter results...' } = icon('filter') + %button.clear-search.hidden + = icon('times') .pull-right - if boards_page #js-boards-seach.issue-boards-search -- cgit v1.2.1 From 7564c5713319517d0b61bc421275197ae7c79113 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 8 Nov 2016 12:59:30 -0600 Subject: Use + instead of %20 --- .../javascripts/filtered_search/filtered_search_manager.js.es6 | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index ad988fe2072..fccc0de050f 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -70,8 +70,8 @@ const sanitizedKey = key.slice(0, key.indexOf('_')); inputValue += `${sanitizedKey}:${value} `; } else if (!match && key === 'search') { - // Sanitize value as URL converts spaces into %20 - const sanitizedValue = value.replace('%20', ' '); + // Sanitize value as URL converts spaces into + + const sanitizedValue = value.replace(/[+]/g, ' '); inputValue += `${sanitizedValue} `; } }); @@ -149,7 +149,6 @@ console.log('search'); let path = '?scope=all&state=opened&utf8=âś“'; - this.tokens.forEach((token) => { const param = validTokenKeys.find((t) => { return t.key === token.key; @@ -159,7 +158,7 @@ }); if (this.searchToken) { - path += '&search=' + this.searchToken; + path += '&search=' + this.searchToken.replace(/ /g, '+'); } window.location = path; -- cgit v1.2.1 From 6b4358eaf70afdd79e441501a5b41690ef70b845 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 8 Nov 2016 13:20:37 -0600 Subject: Add search based on state --- .../filtered_search/filtered_search_manager.js.es6 | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index fccc0de050f..63cdcecdf49 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -147,7 +147,22 @@ search() { console.log('search'); - let path = '?scope=all&state=opened&utf8=âś“'; + let path = '?scope=all&utf8=âś“'; + + // Check current state + const currentPath = window.location.search; + const stateIndex = currentPath.indexOf('state='); + const defaultState = 'opened'; + let currentState = defaultState; + + if (stateIndex !== -1) { + const remaining = currentPath.slice(stateIndex + 6); + const separatorIndex = remaining.indexOf('&'); + + currentState = separatorIndex === -1 ? remaining : remaining.slice(0, separatorIndex); + } + + path += `&state=${currentState}` this.tokens.forEach((token) => { const param = validTokenKeys.find((t) => { -- cgit v1.2.1 From 7b382af73956518b73872c9638754e86da15d915 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 8 Nov 2016 14:18:46 -0600 Subject: Add support for quotations --- .../filtered_search/filtered_search_manager.js.es6 | 41 +++++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 63cdcecdf49..f5e53d075b0 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -68,7 +68,13 @@ if (match) { const sanitizedKey = key.slice(0, key.indexOf('_')); - inputValue += `${sanitizedKey}:${value} `; + let sanitizedValue = value; + + if (match && sanitizedKey === 'label') { + sanitizedValue = sanitizedValue.replace(/%20/g, ' '); + } + + inputValue += `${sanitizedKey}:${sanitizedValue} `; } else if (!match && key === 'search') { // Sanitize value as URL converts spaces into + const sanitizedValue = value.replace(/[+]/g, ' '); @@ -91,26 +97,51 @@ // Enable clear button document.querySelector('.clear-search').classList.remove('hidden'); - // TODO: Current implementation does not support token values that have valid spaces in them - // Example/ label:community contribution const input = event.target.value; const inputs = input.split(' '); let searchTerms = ''; + let lastQuotation = ''; + let incompleteToken = false; const addSearchTerm = function addSearchTerm(term) { searchTerms += term + ' '; } inputs.forEach((i) => { + if (incompleteToken) { + const prevToken = this.tokens[this.tokens.length - 1]; + prevToken.value += ` ${i}`; + + // Remove last quotation + const lastQuotationRegex = new RegExp(lastQuotation, 'g'); + prevToken.value = prevToken.value.replace(lastQuotationRegex, ''); + this.tokens[this.tokens.length - 1] = prevToken; + + // Check to see if this quotation completes the token value + if (i.indexOf(lastQuotation)) { + incompleteToken = !incompleteToken; + } + + return; + } + const colonIndex = i.indexOf(':'); if (colonIndex !== -1) { const tokenKey = i.slice(0, colonIndex).toLowerCase(); const tokenValue = i.slice(colonIndex + 1); - const match = validTokenKeys.filter((v) => { + const match = validTokenKeys.find((v) => { return v.key === tokenKey; - })[0]; + }); + + if (tokenValue.indexOf('"') !== -1) { + lastQuotation = '"'; + incompleteToken = true; + } else if (tokenValue.indexOf('\'') !== -1) { + lastQuotation = '\''; + incompleteToken = true; + } if (match && tokenValue.length > 0) { this.tokens.push({ -- cgit v1.2.1 From 53b4d1b3a76f2aa80070699f623c90e4f7766506 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 8 Nov 2016 15:42:15 -0600 Subject: Add special character encoding --- .../javascripts/filtered_search/filtered_search_manager.js.es6 | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index f5e53d075b0..393e0b8a4b2 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -17,7 +17,7 @@ },{ key: 'label', type: 'array', - param: 'name%5B%5D', + param: 'name[]', },]; class FilteredSearchManager { @@ -54,13 +54,14 @@ } loadSearchParamsFromURL() { + // We can trust that each param has one & since values containing & will be encoded const params = window.location.search.split('&'); let inputValue = ''; params.forEach((p) => { const split = p.split('='); const key = split[0]; - const value = split[1]; + const value = decodeURIComponent(split[1]); const match = validTokenKeys.find((t) => { return key === `${t.key}_${t.param}`; @@ -200,11 +201,11 @@ return t.key === token.key; }).param; - path += `&${token.key}_${param}=${token.value}`; + path += `&${token.key}_${param}=${encodeURIComponent(token.value)}`; }); if (this.searchToken) { - path += '&search=' + this.searchToken.replace(/ /g, '+'); + path += '&search=' + encodeURIComponent(this.searchToken.replace(/ /g, '+')); } window.location = path; -- cgit v1.2.1 From 3ce7f23a855e9061e8f702ae9c9c07ce91e24738 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 8 Nov 2016 16:20:47 -0600 Subject: Fix bug where search terms would not work after switching to another state tab --- .../filtered_search/filtered_search_manager.js.es6 | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 393e0b8a4b2..3528d9da88c 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -55,12 +55,13 @@ loadSearchParamsFromURL() { // We can trust that each param has one & since values containing & will be encoded - const params = window.location.search.split('&'); + // Remove the first character of search as it is always ? + const params = window.location.search.slice(1).split('&'); let inputValue = ''; params.forEach((p) => { const split = p.split('='); - const key = split[0]; + const key = decodeURIComponent(split[0]); const value = decodeURIComponent(split[1]); const match = validTokenKeys.find((t) => { @@ -69,17 +70,24 @@ if (match) { const sanitizedKey = key.slice(0, key.indexOf('_')); - let sanitizedValue = value; + const valueHasSpace = value.indexOf(' ') !== -1; - if (match && sanitizedKey === 'label') { - sanitizedValue = sanitizedValue.replace(/%20/g, ' '); + const preferredQuotations = '"'; + let quotationsToUse = preferredQuotations; + + if (valueHasSpace) { + // Prefer ", but use ' if required + quotationsToUse = value.indexOf(preferredQuotations) === -1 ? preferredQuotations : '\''; } - inputValue += `${sanitizedKey}:${sanitizedValue} `; + inputValue += valueHasSpace ? `${sanitizedKey}:${quotationsToUse}${value}${quotationsToUse}` : `${sanitizedKey}:${value}`; + inputValue += ' '; + } else if (!match && key === 'search') { // Sanitize value as URL converts spaces into + const sanitizedValue = value.replace(/[+]/g, ' '); - inputValue += `${sanitizedValue} `; + inputValue += sanitizedValue; + inputValue += ' '; } }); -- cgit v1.2.1 From d797b03b98e9eccc5d2c7ba4de2d46b0aff1ff67 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 8 Nov 2016 16:30:06 -0600 Subject: Fix bug where spaces would conver into + for all values --- .../filtered_search/filtered_search_manager.js.es6 | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 3528d9da88c..9fe70bbf3a7 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -62,7 +62,11 @@ params.forEach((p) => { const split = p.split('='); const key = decodeURIComponent(split[0]); - const value = decodeURIComponent(split[1]); + const value = split[1]; + + // Sanitize value since URL converts spaces into + + // Replace before decode so that we know what was originally + versus the encoded + + const sanitizedValue = value ? decodeURIComponent(value.replace(/[+]/g, ' ')) : value; const match = validTokenKeys.find((t) => { return key === `${t.key}_${t.param}`; @@ -70,22 +74,20 @@ if (match) { const sanitizedKey = key.slice(0, key.indexOf('_')); - const valueHasSpace = value.indexOf(' ') !== -1; + const valueHasSpace = sanitizedValue.indexOf(' ') !== -1; const preferredQuotations = '"'; let quotationsToUse = preferredQuotations; if (valueHasSpace) { // Prefer ", but use ' if required - quotationsToUse = value.indexOf(preferredQuotations) === -1 ? preferredQuotations : '\''; + quotationsToUse = sanitizedValue.indexOf(preferredQuotations) === -1 ? preferredQuotations : '\''; } - inputValue += valueHasSpace ? `${sanitizedKey}:${quotationsToUse}${value}${quotationsToUse}` : `${sanitizedKey}:${value}`; + inputValue += valueHasSpace ? `${sanitizedKey}:${quotationsToUse}${sanitizedValue}${quotationsToUse}` : `${sanitizedKey}:${sanitizedValue}`; inputValue += ' '; } else if (!match && key === 'search') { - // Sanitize value as URL converts spaces into + - const sanitizedValue = value.replace(/[+]/g, ' '); inputValue += sanitizedValue; inputValue += ' '; } @@ -213,7 +215,7 @@ }); if (this.searchToken) { - path += '&search=' + encodeURIComponent(this.searchToken.replace(/ /g, '+')); + path += '&search=' + encodeURIComponent(this.searchToken); } window.location = path; -- cgit v1.2.1 From ad02257c3ca25806c7104673566bc99c3f6867ed Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 8 Nov 2016 17:54:19 -0600 Subject: Fix bug where clear search button would not toggle visible --- .../filtered_search/filtered_search_manager.js.es6 | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 9fe70bbf3a7..42fe0cace10 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -32,6 +32,7 @@ const clearSearch = document.querySelector('.clear-search'); input.addEventListener('input', this.tokenize.bind(this)); + input.addEventListener('input', this.toggleClearSearchButton); input.addEventListener('keydown', this.checkForEnter.bind(this)); clearSearch.addEventListener('click', this.clearSearch.bind(this)); @@ -42,10 +43,8 @@ event.preventDefault(); this.clearTokens(); - const input = document.querySelector('.filtered-search'); - input.value = ''; - - event.target.classList.add('hidden'); + document.querySelector('.filtered-search').value = ''; + document.querySelector('.clear-search').classList.add('hidden'); } clearTokens() { @@ -101,13 +100,20 @@ } } + toggleClearSearchButton(event) { + const clearSearch = document.querySelector('.clear-search'); + + if (event.target.value) { + clearSearch.classList.remove('hidden'); + } else { + clearSearch.classList.add('hidden'); + } + } + tokenize(event) { // Re-calculate tokens this.clearTokens(); - // Enable clear button - document.querySelector('.clear-search').classList.remove('hidden'); - const input = event.target.value; const inputs = input.split(' '); let searchTerms = ''; -- cgit v1.2.1 From fe4d33cf15b877e8ba22f518f068088db8a3e36d Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 9 Nov 2016 13:51:43 -0600 Subject: Fix scss lint --- app/assets/stylesheets/framework/filters.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index b192455f5f0..90b9394b207 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -39,6 +39,7 @@ color: #444; } } + .fa-filter { position: absolute; top: 10px; @@ -56,7 +57,7 @@ background-color: transparent; border: none; position: absolute; - right: 0px; + right: 0; height: 100%; outline: none; -- cgit v1.2.1 From f1574e45b268e9f1dd488a3962327a4c40f26ae9 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 9 Nov 2016 14:31:58 -0600 Subject: Fix ESLint errors --- .../filtered_search/filtered_search_manager.js.es6 | 156 ++++++++++----------- 1 file changed, 71 insertions(+), 85 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 42fe0cace10..1b58fc01608 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -1,29 +1,81 @@ +/* eslint-disable no-param-reassign */ ((global) => { - const TOKEN_TYPE_STRING = 'string'; - const TOKEN_TYPE_ARRAY = 'array'; - const validTokenKeys = [{ key: 'author', type: 'string', param: 'username', - },{ + }, { key: 'assignee', type: 'string', param: 'username', - },{ + }, { key: 'milestone', type: 'string', param: 'title', - },{ + }, { key: 'label', type: 'array', param: 'name[]', - },]; + }]; + + function toggleClearSearchButton(event) { + const clearSearch = document.querySelector('.clear-search'); + + if (event.target.value) { + clearSearch.classList.remove('hidden'); + } else { + clearSearch.classList.add('hidden'); + } + } + + function loadSearchParamsFromURL() { + // We can trust that each param has one & since values containing & will be encoded + // Remove the first character of search as it is always ? + const params = window.location.search.slice(1).split('&'); + let inputValue = ''; + + params.forEach((p) => { + const split = p.split('='); + const key = decodeURIComponent(split[0]); + const value = split[1]; + + // Sanitize value since URL converts spaces into + + // Replace before decode so that we know what was originally + versus the encoded + + const sanitizedValue = value ? decodeURIComponent(value.replace(/[+]/g, ' ')) : value; + const match = validTokenKeys.find(t => key === `${t.key}_${t.param}`); + + if (match) { + const sanitizedKey = key.slice(0, key.indexOf('_')); + const valueHasSpace = sanitizedValue.indexOf(' ') !== -1; + + const preferredQuotations = '"'; + let quotationsToUse = preferredQuotations; + + if (valueHasSpace) { + // Prefer ", but use ' if required + quotationsToUse = sanitizedValue.indexOf(preferredQuotations) === -1 ? preferredQuotations : '\''; + } + + inputValue += valueHasSpace ? `${sanitizedKey}:${quotationsToUse}${sanitizedValue}${quotationsToUse}` : `${sanitizedKey}:${sanitizedValue}`; + inputValue += ' '; + } else if (!match && key === 'search') { + inputValue += sanitizedValue; + inputValue += ' '; + } + }); + + // Trim the last space value + document.querySelector('.filtered-search').value = inputValue.trim(); + + if (inputValue.trim()) { + document.querySelector('.clear-search').classList.remove('hidden'); + } + } class FilteredSearchManager { constructor() { this.bindEvents(); - this.loadSearchParamsFromURL(); + loadSearchParamsFromURL(); this.clearTokens(); } @@ -32,7 +84,7 @@ const clearSearch = document.querySelector('.clear-search'); input.addEventListener('input', this.tokenize.bind(this)); - input.addEventListener('input', this.toggleClearSearchButton); + input.addEventListener('input', toggleClearSearchButton); input.addEventListener('keydown', this.checkForEnter.bind(this)); clearSearch.addEventListener('click', this.clearSearch.bind(this)); @@ -52,64 +104,6 @@ this.searchToken = ''; } - loadSearchParamsFromURL() { - // We can trust that each param has one & since values containing & will be encoded - // Remove the first character of search as it is always ? - const params = window.location.search.slice(1).split('&'); - let inputValue = ''; - - params.forEach((p) => { - const split = p.split('='); - const key = decodeURIComponent(split[0]); - const value = split[1]; - - // Sanitize value since URL converts spaces into + - // Replace before decode so that we know what was originally + versus the encoded + - const sanitizedValue = value ? decodeURIComponent(value.replace(/[+]/g, ' ')) : value; - - const match = validTokenKeys.find((t) => { - return key === `${t.key}_${t.param}`; - }); - - if (match) { - const sanitizedKey = key.slice(0, key.indexOf('_')); - const valueHasSpace = sanitizedValue.indexOf(' ') !== -1; - - const preferredQuotations = '"'; - let quotationsToUse = preferredQuotations; - - if (valueHasSpace) { - // Prefer ", but use ' if required - quotationsToUse = sanitizedValue.indexOf(preferredQuotations) === -1 ? preferredQuotations : '\''; - } - - inputValue += valueHasSpace ? `${sanitizedKey}:${quotationsToUse}${sanitizedValue}${quotationsToUse}` : `${sanitizedKey}:${sanitizedValue}`; - inputValue += ' '; - - } else if (!match && key === 'search') { - inputValue += sanitizedValue; - inputValue += ' '; - } - }); - - // Trim the last space value - document.querySelector('.filtered-search').value = inputValue.trim(); - - if (inputValue.trim()) { - document.querySelector('.clear-search').classList.remove('hidden'); - } - } - - toggleClearSearchButton(event) { - const clearSearch = document.querySelector('.clear-search'); - - if (event.target.value) { - clearSearch.classList.remove('hidden'); - } else { - clearSearch.classList.add('hidden'); - } - } - tokenize(event) { // Re-calculate tokens this.clearTokens(); @@ -121,8 +115,9 @@ let incompleteToken = false; const addSearchTerm = function addSearchTerm(term) { - searchTerms += term + ' '; - } + // Add space for next term + searchTerms += `${term} `; + }; inputs.forEach((i) => { if (incompleteToken) { @@ -147,10 +142,7 @@ if (colonIndex !== -1) { const tokenKey = i.slice(0, colonIndex).toLowerCase(); const tokenValue = i.slice(colonIndex + 1); - - const match = validTokenKeys.find((v) => { - return v.key === tokenKey; - }); + const match = validTokenKeys.find(v => v.key === tokenKey); if (tokenValue.indexOf('"') !== -1) { lastQuotation = '"'; @@ -178,11 +170,9 @@ } printTokens() { - console.log('tokens:') - this.tokens.forEach((token) => { - console.log(token); - }) - console.log('search: ' + this.searchToken); + console.log('tokens:'); + this.tokens.forEach(token => console.log(token)); + console.log(`search: ${this.searchToken}`); } checkForEnter(event) { @@ -210,18 +200,14 @@ currentState = separatorIndex === -1 ? remaining : remaining.slice(0, separatorIndex); } - path += `&state=${currentState}` - + path += `&state=${currentState}`; this.tokens.forEach((token) => { - const param = validTokenKeys.find((t) => { - return t.key === token.key; - }).param; - + const param = validTokenKeys.find(t => t.key === token.key).param; path += `&${token.key}_${param}=${encodeURIComponent(token.value)}`; }); if (this.searchToken) { - path += '&search=' + encodeURIComponent(this.searchToken); + path += `&search=${encodeURIComponent(this.searchToken)}`; } window.location = path; -- cgit v1.2.1 From 3845bf377296f58e1604d44e4db529099e14888e Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 9 Nov 2016 14:44:11 -0600 Subject: Add droplab --- app/assets/javascripts/droplab/droplab.js | 515 +++++++++++++++++++++++ app/assets/javascripts/droplab/droplab_ajax.js | 45 ++ app/assets/javascripts/droplab/droplab_filter.js | 28 ++ 3 files changed, 588 insertions(+) create mode 100644 app/assets/javascripts/droplab/droplab.js create mode 100644 app/assets/javascripts/droplab/droplab_ajax.js create mode 100644 app/assets/javascripts/droplab/droplab_filter.js diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js new file mode 100644 index 00000000000..18ca8be7203 --- /dev/null +++ b/app/assets/javascripts/droplab/droplab.js @@ -0,0 +1,515 @@ +/* eslint-disable */ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.droplab = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o0){ + if(!listItems[currentIndex-1]){ + currentIndex = currentIndex-1; + } + listItems[currentIndex-1].classList.add('dropdown-active'); + } + }; + + var mousedown = function mousedown(e) { + var list = e.detail.hook.list; + removeHighlight(list); + list.show(); + currentIndex = 0; + isUpArrow = false; + isDownArrow = false; + }; + var selectItem = function selectItem(list) { + var listItems = removeHighlight(list); + var currentItem = listItems[currentIndex-1]; + var listEvent = new CustomEvent('click.dl', { + detail: { + list: list, + selected: currentItem, + data: currentItem.dataset, + }, + }); + list.list.dispatchEvent(listEvent); + list.hide(); + } + + var keydown = function keydown(e){ + var typedOn = e.target; + isUpArrow = false; + isDownArrow = false; + + if(e.detail.which){ + currentKey = e.detail.which; + if(currentKey === 13){ + selectItem(e.detail.hook.list); + return; + } + if(currentKey === 38) { + isUpArrow = true; + } + if(currentKey === 40) { + isDownArrow = true; + } + } else if(e.detail.key) { + currentKey = e.detail.key; + if(currentKey === 'Enter'){ + selectItem(e.detail.hook.list); + return; + } + if(currentKey === 'ArrowUp') { + isUpArrow = true; + } + if(currentKey === 'ArrowDown') { + isDownArrow = true; + } + } + if(isUpArrow){ currentIndex--; } + if(isDownArrow){ currentIndex++; } + if(currentIndex < 0){ currentIndex = 0; } + setMenuForArrows(e.detail.hook.list); + }; + + w.addEventListener('mousedown.dl', mousedown); + w.addEventListener('keydown.dl', keydown); + }; +}); +},{"./window":11}],10:[function(require,module,exports){ +var DATA_TRIGGER = require('./constants').DATA_TRIGGER; +var DATA_DROPDOWN = require('./constants').DATA_DROPDOWN; + +var toDataCamelCase = function(attr){ + return this.camelize(attr.split('-').slice(1).join(' ')); +}; + +// the tiniest damn templating I can do +var t = function(s,d){ + for(var p in d) + s=s.replace(new RegExp('{{'+p+'}}','g'), d[p]); + return s; +}; + +var camelize = function(str) { + return str.replace(/(?:^\w|[A-Z]|\b\w)/g, function(letter, index) { + return index == 0 ? letter.toLowerCase() : letter.toUpperCase(); + }).replace(/\s+/g, ''); +}; + +var closest = function(thisTag, stopTag) { + while(thisTag.tagName !== stopTag && thisTag.tagName !== 'HTML'){ + thisTag = thisTag.parentNode; + } + return thisTag; +}; + +var isDropDownParts = function(target) { + if(target.tagName === 'HTML') { return false; } + return ( + target.hasAttribute(DATA_TRIGGER) || + target.hasAttribute(DATA_DROPDOWN) + ); +}; + +module.exports = { + toDataCamelCase: toDataCamelCase, + t: t, + camelize: camelize, + closest: closest, + isDropDownParts: isDropDownParts, +}; + +},{"./constants":1}],11:[function(require,module,exports){ +module.exports = function(callback) { + return (function() { + callback(this); + }).call(null); +}; + +},{}]},{},[8])(8) +}); diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js new file mode 100644 index 00000000000..23e43b352d6 --- /dev/null +++ b/app/assets/javascripts/droplab/droplab_ajax.js @@ -0,0 +1,45 @@ +/* eslint-disable */ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.ajax||(g.ajax = {}));g=(g.datasource||(g.datasource = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o Date: Wed, 9 Nov 2016 17:07:30 -0600 Subject: Refactor tokenizer --- .../filtered_search/filtered_search_manager.js.es6 | 120 +++++---------------- .../filtered_search/filtered_search_tokenizer.es6 | 90 ++++++++++++++++ 2 files changed, 115 insertions(+), 95 deletions(-) create mode 100644 app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 1b58fc01608..58c64ea078d 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -18,13 +18,21 @@ param: 'name[]', }]; + function clearSearch(event) { + event.stopPropagation(); + event.preventDefault(); + + document.querySelector('.filtered-search').value = ''; + document.querySelector('.clear-search').classList.add('hidden'); + } + function toggleClearSearchButton(event) { - const clearSearch = document.querySelector('.clear-search'); + const clearSearchButton = document.querySelector('.clear-search'); if (event.target.value) { - clearSearch.classList.remove('hidden'); + clearSearchButton.classList.remove('hidden'); } else { - clearSearch.classList.add('hidden'); + clearSearchButton.classList.add('hidden'); } } @@ -74,105 +82,24 @@ class FilteredSearchManager { constructor() { + this.tokenizer = new gl.FilteredSearchTokenizer(validTokenKeys); this.bindEvents(); loadSearchParamsFromURL(); - this.clearTokens(); } bindEvents() { - const input = document.querySelector('.filtered-search'); - const clearSearch = document.querySelector('.clear-search'); - - input.addEventListener('input', this.tokenize.bind(this)); - input.addEventListener('input', toggleClearSearchButton); - input.addEventListener('keydown', this.checkForEnter.bind(this)); + const filteredSearchInput = document.querySelector('.filtered-search'); - clearSearch.addEventListener('click', this.clearSearch.bind(this)); - } - - clearSearch(event) { - event.stopPropagation(); - event.preventDefault(); - - this.clearTokens(); - document.querySelector('.filtered-search').value = ''; - document.querySelector('.clear-search').classList.add('hidden'); - } + filteredSearchInput.addEventListener('input', this.processInput.bind(this)); + filteredSearchInput.addEventListener('input', toggleClearSearchButton); + filteredSearchInput.addEventListener('keydown', this.checkForEnter.bind(this)); - clearTokens() { - this.tokens = []; - this.searchToken = ''; + document.querySelector('.clear-search').addEventListener('click', clearSearch); } - tokenize(event) { - // Re-calculate tokens - this.clearTokens(); - + processInput(event) { const input = event.target.value; - const inputs = input.split(' '); - let searchTerms = ''; - let lastQuotation = ''; - let incompleteToken = false; - - const addSearchTerm = function addSearchTerm(term) { - // Add space for next term - searchTerms += `${term} `; - }; - - inputs.forEach((i) => { - if (incompleteToken) { - const prevToken = this.tokens[this.tokens.length - 1]; - prevToken.value += ` ${i}`; - - // Remove last quotation - const lastQuotationRegex = new RegExp(lastQuotation, 'g'); - prevToken.value = prevToken.value.replace(lastQuotationRegex, ''); - this.tokens[this.tokens.length - 1] = prevToken; - - // Check to see if this quotation completes the token value - if (i.indexOf(lastQuotation)) { - incompleteToken = !incompleteToken; - } - - return; - } - - const colonIndex = i.indexOf(':'); - - if (colonIndex !== -1) { - const tokenKey = i.slice(0, colonIndex).toLowerCase(); - const tokenValue = i.slice(colonIndex + 1); - const match = validTokenKeys.find(v => v.key === tokenKey); - - if (tokenValue.indexOf('"') !== -1) { - lastQuotation = '"'; - incompleteToken = true; - } else if (tokenValue.indexOf('\'') !== -1) { - lastQuotation = '\''; - incompleteToken = true; - } - - if (match && tokenValue.length > 0) { - this.tokens.push({ - key: match.key, - value: tokenValue, - }); - } else { - addSearchTerm(i); - } - } else { - addSearchTerm(i); - } - }, this); - - this.searchToken = searchTerms.trim(); - this.printTokens(); - } - - printTokens() { - console.log('tokens:'); - this.tokens.forEach(token => console.log(token)); - console.log(`search: ${this.searchToken}`); + this.tokenizer.processTokens(input); } checkForEnter(event) { @@ -193,6 +120,9 @@ const defaultState = 'opened'; let currentState = defaultState; + const tokens = this.tokenizer.getTokens(); + const searchToken = this.tokenizer.getSearchToken(); + if (stateIndex !== -1) { const remaining = currentPath.slice(stateIndex + 6); const separatorIndex = remaining.indexOf('&'); @@ -201,13 +131,13 @@ } path += `&state=${currentState}`; - this.tokens.forEach((token) => { + tokens.forEach((token) => { const param = validTokenKeys.find(t => t.key === token.key).param; path += `&${token.key}_${param}=${encodeURIComponent(token.value)}`; }); - if (this.searchToken) { - path += `&search=${encodeURIComponent(this.searchToken)}`; + if (searchToken) { + path += `&search=${encodeURIComponent(searchToken)}`; } window.location = path; diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 new file mode 100644 index 00000000000..f6cc1b8860d --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 @@ -0,0 +1,90 @@ +/* eslint-disable no-param-reassign */ +((global) => { + class FilteredSearchTokenizer { + constructor(validTokenKeys) { + this.validTokenKeys = validTokenKeys; + this.resetTokens(); + } + + getTokens() { + return this.tokens; + } + + getSearchToken() { + return this.searchToken; + } + + resetTokens() { + this.tokens = []; + this.searchToken = ''; + } + + printTokens() { + console.log('tokens:'); + this.tokens.forEach(token => console.log(token)); + console.log(`search: ${this.searchToken}`); + } + + processTokens(input) { + // Re-calculate tokens + this.resetTokens(); + + const inputs = input.split(' '); + let searchTerms = ''; + let lastQuotation = ''; + let incompleteToken = false; + + inputs.forEach((i) => { + if (incompleteToken) { + const prevToken = this.tokens[this.tokens.length - 1]; + prevToken.value += ` ${i}`; + + // Remove last quotation + const lastQuotationRegex = new RegExp(lastQuotation, 'g'); + prevToken.value = prevToken.value.replace(lastQuotationRegex, ''); + this.tokens[this.tokens.length - 1] = prevToken; + + // Check to see if this quotation completes the token value + if (i.indexOf(lastQuotation)) { + incompleteToken = !incompleteToken; + } + + return; + } + + const colonIndex = i.indexOf(':'); + + if (colonIndex !== -1) { + const tokenKey = i.slice(0, colonIndex).toLowerCase(); + const tokenValue = i.slice(colonIndex + 1); + const match = this.validTokenKeys.find(v => v.key === tokenKey); + + if (tokenValue.indexOf('"') !== -1) { + lastQuotation = '"'; + incompleteToken = true; + } else if (tokenValue.indexOf('\'') !== -1) { + lastQuotation = '\''; + incompleteToken = true; + } + + if (match && tokenValue.length > 0) { + this.tokens.push({ + key: match.key, + value: tokenValue, + }); + + return; + } + } + + // Add space for next term + searchTerms += `${i} `; + }, this); + + this.searchToken = searchTerms.trim(); + this.printTokens(); + } + } + + global.FilteredSearchTokenizer = FilteredSearchTokenizer; +})(window.gl || (window.gl = {})); -- cgit v1.2.1 From 8b4e4e333db0cf47080aa8577b4351b9e00525ea Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 9 Nov 2016 19:10:15 -0600 Subject: Fix JS for tests --- .../javascripts/filtered_search/filtered_search_manager.js.es6 | 7 ++++--- .../javascripts/filtered_search/filtered_search_tokenizer.es6 | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 58c64ea078d..db414b9755d 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -50,7 +50,7 @@ // Sanitize value since URL converts spaces into + // Replace before decode so that we know what was originally + versus the encoded + const sanitizedValue = value ? decodeURIComponent(value.replace(/[+]/g, ' ')) : value; - const match = validTokenKeys.find(t => key === `${t.key}_${t.param}`); + const match = validTokenKeys.filter(t => key === `${t.key}_${t.param}`)[0]; if (match) { const sanitizedKey = key.slice(0, key.indexOf('_')); @@ -103,7 +103,8 @@ } checkForEnter(event) { - if (event.key === 'Enter') { + // Enter KeyCode + if (event.keyCode === 13) { event.stopPropagation(); event.preventDefault(); this.search(); @@ -132,7 +133,7 @@ path += `&state=${currentState}`; tokens.forEach((token) => { - const param = validTokenKeys.find(t => t.key === token.key).param; + const param = validTokenKeys.filter(t => t.key === token.key)[0].param; path += `&${token.key}_${param}=${encodeURIComponent(token.value)}`; }); diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 index f6cc1b8860d..ddb173b2d98 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 @@ -57,7 +57,7 @@ if (colonIndex !== -1) { const tokenKey = i.slice(0, colonIndex).toLowerCase(); const tokenValue = i.slice(colonIndex + 1); - const match = this.validTokenKeys.find(v => v.key === tokenKey); + const match = this.validTokenKeys.filter(v => v.key === tokenKey)[0]; if (tokenValue.indexOf('"') !== -1) { lastQuotation = '"'; -- cgit v1.2.1 From 9c8a86f60d2d36b628c5275004e4c17aa07aeeeb Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 10 Nov 2016 16:49:12 -0600 Subject: Update filter issue specs --- app/assets/stylesheets/framework/filters.scss | 2 + spec/features/issues/filter_issues_spec.rb | 506 ++++++++++++++------------ 2 files changed, 273 insertions(+), 235 deletions(-) diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 90b9394b207..c679a3833e9 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -24,10 +24,12 @@ } .filtered-search-container { + display: -webkit-flex; display: flex; } .filtered-search-input-container { + display: -webkit-flex; display: flex; position: relative; width: 100%; diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index 0d19563d628..7d681742045 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -4,147 +4,236 @@ describe 'Filter issues', feature: true do include WaitForAjax let!(:group) { create(:group) } - let!(:project) { create(:project, group: group) } + let!(:project) { create(:project) } let!(:user) { create(:user)} + let!(:user) { create(:user) } + let!(:user2) { create(:user) } let!(:milestone) { create(:milestone, project: project) } let!(:label) { create(:label, project: project) } let!(:wontfix) { create(:label, project: project, title: "Won't fix") } + let!(:bug_label) { create(:label, project: project, title: 'bug') } + let!(:caps_sensitive_label) { create(:label, project: project, title: 'CAPS_sensitive') } + let!(:milestone) { create(:milestone, title: "8", project: project) } + + def input_filtered_search(search_term) + filtered_search = find('.filtered-search') + filtered_search.set(search_term) + filtered_search.send_keys(:enter) + end + + def expect_no_issues_list + page.within '.issues-list' do + expect(page).not_to have_selector('.issue') + end + end + + def expect_issues_list_count(open_count, closed_count = 0) + all_count = open_count + closed_count + + expect(page).to have_issuable_counts(open: open_count, closed: closed_count, all: all_count) + page.within '.issues-list' do + expect(page).to have_selector('.issue', count: open_count) + end + end + before do project.team << [user, :master] + project.team << [user2, :master] group.add_developer(user) + group.add_developer(user2) login_as(user) create(:issue, project: project) - end - describe 'for assignee from issues#index' do - before do - visit namespace_project_issues_path(project.namespace, project) + create(:issue, title: "Bug report 1", project: project) + create(:issue, title: "Bug report 2", project: project) + create(:issue, title: "issue with 'single quotes'", project: project) + create(:issue, title: "issue with \"double quotes\"", project: project) + create(:issue, title: "issue with !@\#{$%^&*()-+", project: project) + create(:issue, title: "issue by assignee", project: project, milestone: milestone, author: user, assignee: user) + create(:issue, title: "issue by assignee with searchTerm", project: project, milestone: milestone, author: user, assignee: user) + + issue = create(:issue, + title: "Bug 2", + project: project, + milestone: milestone, + author: user, + assignee: user) + issue.labels << bug_label + + issue_with_caps_label = create(:issue, + title: "issue by assignee with searchTerm and label", + project: project, + milestone: milestone, + author: user, + assignee: user) + issue_with_caps_label.labels << caps_sensitive_label + + issue_with_everything = create(:issue, + title: "Bug report with everything you thought was possible", + project: project, + milestone: milestone, + author: user, + assignee: user) + issue_with_everything.labels << bug_label + issue_with_everything.labels << caps_sensitive_label + + visit namespace_project_issues_path(project.namespace, project) + end - find('.js-assignee-search').click + describe 'filter issues by author' do + context 'only author', js: true do + it 'filters issues by searched author' do + input_filtered_search("author:#{user.username}") + expect_issues_list_count(5) + end - find('.dropdown-menu-user-link', text: user.username).click + it 'filters issues by invalid author' do + # YOLO + end - wait_for_ajax + it 'filters issues by multiple authors' do + # YOLO + end end - context 'assignee', js: true do - it 'updates to current user' do - expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + context 'author with other filters', js: true do + it 'filters issues by searched author and text' do + input_filtered_search("author:#{user.username} issue") + expect_issues_list_count(3) end - it 'does not change when closed link is clicked' do - find('.issues-state-filters a', text: "Closed").click - - expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + it 'filters issues by searched author, assignee and text' do + input_filtered_search("author:#{user.username} assignee:#{user.username} issue") + expect_issues_list_count(3) end - it 'does not change when all link is clicked' do - find('.issues-state-filters a', text: "All").click + it 'filters issues by searched author, assignee, label, and text' do + input_filtered_search("author:#{user.username} assignee:#{user.username} label:#{caps_sensitive_label.title} issue") + expect_issues_list_count(1) + end - expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + it 'filters issues by searched author, assignee, label, milestone and text' do + input_filtered_search("author:#{user.username} assignee:#{user.username} label:#{caps_sensitive_label.title} milestone:#{milestone.title} issue") + expect_issues_list_count(1) end end + + context 'sorting', js: true do + # TODO + end end - describe 'for milestone from issues#index' do - before do - visit namespace_project_issues_path(project.namespace, project) + describe 'filter issues by assignee' do + context 'only assignee', js: true do + it 'filters issues by searched assignee' do + input_filtered_search("assignee:#{user.username}") + expect_issues_list_count(5) + end - find('.js-milestone-select').click + it 'filters issues by no assignee' do + # TODO + end - find('.milestone-filter .dropdown-content a', text: milestone.title).click + it 'filters issues by invalid assignee' do + # YOLO + end - wait_for_ajax + it 'filters issues by multiple assignees' do + # YOLO + end end - context 'milestone', js: true do - it 'updates to current milestone' do - expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title) + context 'assignee with other filters', js: true do + it 'filters issues by searched assignee and text' do + input_filtered_search("assignee:#{user.username} searchTerm") + expect_issues_list_count(2) end - it 'does not change when closed link is clicked' do - find('.issues-state-filters a', text: "Closed").click - - expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title) + it 'filters issues by searched assignee, author and text' do + input_filtered_search("assignee:#{user.username} author:#{user.username} searchTerm") + expect_issues_list_count(2) end - it 'does not change when all link is clicked' do - find('.issues-state-filters a', text: "All").click - - expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title) + it 'filters issues by searched assignee, author, label, text' do + input_filtered_search("assignee:#{user.username} author:#{user.username} label:#{caps_sensitive_label.title} searchTerm") + expect_issues_list_count(1) end - end - end - describe 'for label from issues#index', js: true do - before do - visit namespace_project_issues_path(project.namespace, project) - find('.js-label-select').click - wait_for_ajax + it 'filters issues by searched assignee, author, label, milestone and text' do + input_filtered_search("assignee:#{user.username} author:#{user.username} label:#{caps_sensitive_label.title} milestone:#{milestone.title} searchTerm") + expect_issues_list_count(1) + end end - it 'filters by any label' do - find('.dropdown-menu-labels a', text: 'Any Label').click - page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click - wait_for_ajax - - expect(find('.labels-filter')).to have_content 'Label' + context 'sorting', js: true do + # TODO end + end - it 'filters by no label' do - find('.dropdown-menu-labels a', text: 'No Label').click - page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click - wait_for_ajax - - page.within '.labels-filter' do - expect(page).to have_content 'Labels' + describe 'filter issues by label' do + context 'only label', js: true do + it 'filters issues by searched label' do + input_filtered_search("label:#{bug_label.title}") + expect_issues_list_count(2) end - expect(find('.js-label-select .dropdown-toggle-text')).to have_content('Labels') - end - it 'filters by a label' do - find('.dropdown-menu-labels a', text: label.title).click - page.within '.labels-filter' do - expect(page).to have_content label.title + it 'filters issues by no label' do + # TODO end - expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title) - end - it "filters by `won't fix` and another label" do - page.within '.labels-filter' do - click_link wontfix.title - expect(page).to have_content wontfix.title - click_link label.title + it 'filters issues by invalid label' do + # YOLO end - expect(find('.js-label-select .dropdown-toggle-text')).to have_content("#{wontfix.title} +1 more") + it 'filters issues by multiple labels' do + input_filtered_search("label:#{bug_label.title} label:#{caps_sensitive_label.title}") + expect_issues_list_count(1) + end end - it "filters by `won't fix` label followed by another label after page load" do - page.within '.labels-filter' do - click_link wontfix.title - expect(page).to have_content wontfix.title + context 'label with other filters', js: true do + it 'filters issues by searched label and text' do + input_filtered_search("label:#{caps_sensitive_label.title} bug") + expect_issues_list_count(1) end - find('.dropdown-menu-close-icon').click + it 'filters issues by searched label, author and text' do + input_filtered_search("label:#{caps_sensitive_label.title} author:#{user.username} bug") + expect_issues_list_count(1) + end - expect(find('.filtered-labels')).to have_content(wontfix.title) + it 'filters issues by searched label, author, assignee and text' do + input_filtered_search("label:#{caps_sensitive_label.title} author:#{user.username} assignee:#{user.username} bug") + expect_issues_list_count(1) + end - find('.js-label-select').click - wait_for_ajax - find('.dropdown-menu-labels a', text: label.title).click + it 'filters issues by searched label, author, assignee, milestone and text' do + input_filtered_search("label:#{caps_sensitive_label.title} author:#{user.username} assignee:#{user.username} milestone:#{milestone.title} bug") + expect_issues_list_count(1) + end + end - find('.dropdown-menu-close-icon').click + context 'multiple labels with other filters', js: true do + it 'filters issues by searched label, label2, and text' do + input_filtered_search("label:#{bug_label.title} label:#{caps_sensitive_label.title} bug") + expect_issues_list_count(1) + end - expect(find('.filtered-labels')).to have_content(wontfix.title) - expect(find('.filtered-labels')).to have_content(label.title) + it 'filters issues by searched label, label2, author and text' do + input_filtered_search("label:#{bug_label.title} label:#{caps_sensitive_label.title} author:#{user.username} bug") + expect_issues_list_count(1) + end - find('.js-label-select').click - wait_for_ajax + it 'filters issues by searched label, label2, author, assignee and text' do + input_filtered_search("label:#{bug_label.title} label:#{caps_sensitive_label.title} author:#{user.username} assignee:#{user.username} bug") + expect_issues_list_count(1) + end - expect(find('.dropdown-menu-labels li', text: wontfix.title)).to have_css('.is-active') - expect(find('.dropdown-menu-labels li', text: label.title)).to have_css('.is-active') + it 'filters issues by searched label, label2, author, assignee, milestone and text' do + input_filtered_search("label:#{bug_label.title} label:#{caps_sensitive_label.title} author:#{user.username} assignee:#{user.username} milestone:#{milestone.title} bug") + expect_issues_list_count(1) + end end it "selects and unselects `won't fix`" do @@ -153,211 +242,160 @@ describe 'Filter issues', feature: true do find('.dropdown-menu-close-icon').click expect(page).not_to have_css('.filtered-labels') + context 'sorting', js: true do + # TODO end end - describe 'for assignee and label from issues#index' do - before do - visit namespace_project_issues_path(project.namespace, project) - - find('.js-assignee-search').click - - find('.dropdown-menu-user-link', text: user.username).click + describe 'filter issues by milestone' do + context 'only milestone', js: true do + it 'filters issues by searched milestone' do + input_filtered_search("milestone:#{milestone.title}") + expect_issues_list_count(5) + end - expect(page).not_to have_selector('.issues-list .issue') + it 'filters issues by no milestone' do + # TODO + end - find('.js-label-select').click + it 'filters issues by upcoming milestones' do + # TODO + end - find('.dropdown-menu-labels .dropdown-content a', text: label.title).click - page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click + it 'filters issues by invalid milestones' do + # YOLO + end - wait_for_ajax + it 'filters issues by multiple milestones' do + # YOLO + end end - context 'assignee and label', js: true do - it 'updates to current assignee and label' do - expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) - expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title) + context 'milestone with other filters', js: true do + it 'filters issues by searched milestone and text' do end - it 'does not change when closed link is clicked' do - find('.issues-state-filters a', text: "Closed").click - - expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) - expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title) + it 'filters issues by searched milestone, author and text' do end - it 'does not change when all link is clicked' do - find('.issues-state-filters a', text: "All").click + it 'filters issues by searched milestone, author, assignee and text' do + end - expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) - expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title) + it 'filters issues by searched milestone, author, assignee, label and text' do end end - end - - describe 'filter issues by text' do - before do - create(:issue, title: "Bug", project: project) - - bug_label = create(:label, project: project, title: 'bug') - milestone = create(:milestone, title: "8", project: project) - - issue = create(:issue, - title: "Bug 2", - project: project, - milestone: milestone, - author: user, - assignee: user) - issue.labels << bug_label - visit namespace_project_issues_path(project.namespace, project) + context 'sorting', js: true do + # TODO end + end + describe 'filter issues by text' do context 'only text', js: true do it 'filters issues by searched text' do - fill_in 'issuable_search', with: 'Bug' - - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 2) - end + input_filtered_search('Bug') + expect_issues_list_count(4) end - it 'does not show any issues' do - fill_in 'issuable_search', with: 'testing' - - page.within '.issues-list' do - expect(page).not_to have_selector('.issue') - end + it 'filters issues by multiple searched text' do + input_filtered_search('Bug report') + expect_issues_list_count(3) end - end - context 'text and dropdown options', js: true do - it 'filters by text and label' do - fill_in 'issuable_search', with: 'Bug' - - expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 2) - end - - click_button 'Label' - page.within '.labels-filter' do - click_link 'bug' - end - find('.dropdown-menu-close-icon').click - - expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 1) - end + it 'filters issues by case insensitive searched text' do + input_filtered_search('bug report') + expect_issues_list_count(3) end - it 'filters by text and milestone' do - fill_in 'issuable_search', with: 'Bug' - - expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 2) - end - - click_button 'Milestone' - page.within '.milestone-filter' do - click_link '8' - end - - expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 1) - end + it 'filters issues by searched text containing single quotes' do + input_filtered_search('\'single quotes\'') + expect_issues_list_count(1) end - it 'filters by text and assignee' do - fill_in 'issuable_search', with: 'Bug' - - expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 2) - end + it 'filters issues by searched text containing double quotes' do + input_filtered_search('"double quotes"') + expect_issues_list_count(1) + end - click_button 'Assignee' - page.within '.dropdown-menu-assignee' do - click_link user.name - end + it 'filters issues by searched text containing special characters' do + input_filtered_search('!@#{$%^&*()-+') + expect_issues_list_count(1) + end - expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 1) - end + it 'does not show any issues' do + input_filtered_search('testing') + expect_no_issues_list() end + end - it 'filters by text and author' do - fill_in 'issuable_search', with: 'Bug' + context 'searched text with other filters', js: true do + it 'filters issues by searched text and author' do + input_filtered_search("bug author:#{user.username}") + expect_issues_list_count(2) + end - expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 2) - end + it 'filters issues by searched text, author and more text' do + input_filtered_search("bug author:#{user.username} report") + expect_issues_list_count(1) + end - click_button 'Author' - page.within '.dropdown-menu-author' do - click_link user.name - end + it 'filters issues by searched text, author and assignee' do + input_filtered_search("bug author:#{user.username} assignee:#{user.username}") + expect_issues_list_count(2) + end - expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 1) - end + it 'filters issues by searched text, author, more text and assignee' do + input_filtered_search("bug author:#{user.username} report assignee:#{user.username}") + expect_issues_list_count(1) end - end - end - describe 'filter issues and sort', js: true do - before do - bug_label = create(:label, project: project, title: 'bug') - bug_one = create(:issue, title: "Frontend", project: project) - bug_two = create(:issue, title: "Bug 2", project: project) + it 'filters issues by searched text, author, more text, assignee and even more text' do + input_filtered_search("bug author:#{user.username} report assignee:#{user.username} with") + expect_issues_list_count(1) + end - bug_one.labels << bug_label - bug_two.labels << bug_label + it 'filters issues by searched text, author, assignee and label' do + input_filtered_search("bug author:#{user.username} assignee:#{user.username} label:#{bug_label.title}") + expect_issues_list_count(2) + end - visit namespace_project_issues_path(project.namespace, project) - end + it 'filters issues by searched text, author, text, assignee, text, label and text' do + input_filtered_search("bug author:#{user.username} report assignee:#{user.username} with label:#{bug_label.title} everything") + expect_issues_list_count(1) + end - it 'is able to filter and sort issues' do - click_button 'Label' - wait_for_ajax - page.within '.labels-filter' do - click_link 'bug' + it 'filters issues by searched text, author, assignee, label and milestone' do + input_filtered_search("bug author:#{user.username} assignee:#{user.username} label:#{bug_label.title} milestone:#{milestone.title}") + expect_issues_list_count(2) end - find('.dropdown-menu-close-icon').click - wait_for_ajax - expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 2) + it 'filters issues by searched text, author, text, assignee, text, label, text, milestone and text' do + input_filtered_search("bug author:#{user.username} report assignee:#{user.username} with label:#{bug_label.title} everything milestone:#{milestone.title} you") + expect_issues_list_count(1) end - click_button 'Last created' - page.within '.dropdown-menu-sort' do - click_link 'Oldest created' + it 'filters issues by searched text, author, assignee, multiple labels and milestone' do + input_filtered_search("bug author:#{user.username} assignee:#{user.username} label:#{bug_label.title} label:#{caps_sensitive_label.title} milestone:#{milestone.title}") + expect_issues_list_count(1) end - wait_for_ajax - page.within '.issues-list' do - expect(page).to have_content('Frontend') + it 'filters issues by searched text, author, text, assignee, text, label1, text, label2, text, milestone and text' do + input_filtered_search("bug author:#{user.username} report assignee:#{user.username} with label:#{bug_label.title} everything label:#{caps_sensitive_label.title} you milestone:#{milestone.title} thought") + expect_issues_list_count(1) end end + + context 'sorting', js: true do + # TODO + end end it 'updates atom feed link for project issues' do visit namespace_project_issues_path(project.namespace, project, milestone_title: '', assignee_id: user.id) - link = find('.nav-controls a', text: 'Subscribe') params = CGI::parse(URI.parse(link[:href]).query) auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) auto_discovery_params = CGI::parse(URI.parse(auto_discovery_link[:href]).query) - expect(params).to include('private_token' => [user.private_token]) expect(params).to include('milestone_title' => ['']) expect(params).to include('assignee_id' => [user.id.to_s]) @@ -368,12 +406,10 @@ describe 'Filter issues', feature: true do it 'updates atom feed link for group issues' do visit issues_group_path(group, milestone_title: '', assignee_id: user.id) - link = find('.nav-controls a', text: 'Subscribe') params = CGI::parse(URI.parse(link[:href]).query) auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) auto_discovery_params = CGI::parse(URI.parse(auto_discovery_link[:href]).query) - expect(params).to include('private_token' => [user.private_token]) expect(params).to include('milestone_title' => ['']) expect(params).to include('assignee_id' => [user.id.to_s]) -- cgit v1.2.1 From f20875ec4557b23d6df810bd49e1955f5fbbd6e0 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 11 Nov 2016 11:56:47 -0600 Subject: Add username to gon --- app/assets/javascripts/search_autocomplete.js.es6 | 7 ++++--- lib/gitlab/gon_helper.rb | 1 + spec/features/search_spec.rb | 8 ++++---- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/search_autocomplete.js.es6 b/app/assets/javascripts/search_autocomplete.js.es6 index 437f5dbbf7d..cec8856d4e7 100644 --- a/app/assets/javascripts/search_autocomplete.js.es6 +++ b/app/assets/javascripts/search_autocomplete.js.es6 @@ -142,8 +142,9 @@ } getCategoryContents() { - var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, utils; + var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, userName, utils; userId = gon.current_user_id; + userName = gon.current_username; utils = gl.utils, projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions; if (utils.isInGroupsPage() && groupOptions) { options = groupOptions[utils.getGroupSlug()]; @@ -158,10 +159,10 @@ header: "" + name }, { text: 'Issues assigned to me', - url: issuesPath + "/?assignee_id=" + userId + url: issuesPath + "/?assignee_username=" + userName }, { text: "Issues I've created", - url: issuesPath + "/?author_id=" + userId + url: issuesPath + "/?author_username=" + userName }, 'separator', { text: 'Merge requests assigned to me', url: mrPath + "/?assignee_id=" + userId diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index 4d4e04e9e35..b8a5ac907a4 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -13,6 +13,7 @@ module Gitlab if current_user gon.current_user_id = current_user.id + gon.current_username = current_user.username end end end diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb index caecd027aaa..9a7079848a5 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -169,16 +169,16 @@ describe "Search", feature: true do find('.dropdown-menu').click_link 'Issues assigned to me' sleep 2 - expect(page).to have_selector('.issues-holder') - expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + expect(page).to have_selector('.filtered-search') + expect(find('.filtered-search').value).to eq("assignee:#{user.username}") end it 'takes user to her issues page when issues authored is clicked' do find('.dropdown-menu').click_link "Issues I've created" sleep 2 - expect(page).to have_selector('.issues-holder') - expect(find('.js-author-search .dropdown-toggle-text')).to have_content(user.name) + expect(page).to have_selector('.filtered-search') + expect(find('.filtered-search').value).to eq("author:#{user.username}") end it 'takes user to her MR page when MR assigned is clicked' do -- cgit v1.2.1 From 08728cb3262a4ed9e98920b5f308e84ca6c5749a Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 11 Nov 2016 14:51:32 -0600 Subject: Move spec to check on MR page instead of Issues page --- spec/features/issues/filter_by_labels_spec.rb | 152 -------------------- .../merge_requests/filter_by_labels_spec.rb | 154 +++++++++++++++++++++ 2 files changed, 154 insertions(+), 152 deletions(-) delete mode 100644 spec/features/issues/filter_by_labels_spec.rb create mode 100644 spec/features/merge_requests/filter_by_labels_spec.rb diff --git a/spec/features/issues/filter_by_labels_spec.rb b/spec/features/issues/filter_by_labels_spec.rb deleted file mode 100644 index 0253629f753..00000000000 --- a/spec/features/issues/filter_by_labels_spec.rb +++ /dev/null @@ -1,152 +0,0 @@ -require 'rails_helper' - -feature 'Issue filtering by Labels', feature: true, js: true do - include WaitForAjax - - let(:project) { create(:project, :public) } - let!(:user) { create(:user) } - let!(:label) { create(:label, project: project) } - - before do - bug = create(:label, project: project, title: 'bug') - feature = create(:label, project: project, title: 'feature') - enhancement = create(:label, project: project, title: 'enhancement') - - issue1 = create(:issue, title: "Bugfix1", project: project) - issue1.labels << bug - - issue2 = create(:issue, title: "Bugfix2", project: project) - issue2.labels << bug - issue2.labels << enhancement - - issue3 = create(:issue, title: "Feature1", project: project) - issue3.labels << feature - - project.team << [user, :master] - login_as(user) - - visit namespace_project_issues_path(project.namespace, project) - end - - context 'filter by label bug' do - before do - select_labels('bug') - end - - it 'apply the filter' do - expect(page).to have_content "Bugfix1" - expect(page).to have_content "Bugfix2" - expect(page).not_to have_content "Feature1" - expect(find('.filtered-labels')).to have_content "bug" - expect(find('.filtered-labels')).not_to have_content "feature" - expect(find('.filtered-labels')).not_to have_content "enhancement" - - find('.js-label-filter-remove').click - wait_for_ajax - expect(find('.filtered-labels', visible: false)).to have_no_content "bug" - end - end - - context 'filter by label feature' do - before do - select_labels('feature') - end - - it 'applies the filter' do - expect(page).to have_content "Feature1" - expect(page).not_to have_content "Bugfix2" - expect(page).not_to have_content "Bugfix1" - expect(find('.filtered-labels')).to have_content "feature" - expect(find('.filtered-labels')).not_to have_content "bug" - expect(find('.filtered-labels')).not_to have_content "enhancement" - end - end - - context 'filter by label enhancement' do - before do - select_labels('enhancement') - end - - it 'applies the filter' do - expect(page).to have_content "Bugfix2" - expect(page).not_to have_content "Feature1" - expect(page).not_to have_content "Bugfix1" - expect(find('.filtered-labels')).to have_content "enhancement" - expect(find('.filtered-labels')).not_to have_content "bug" - expect(find('.filtered-labels')).not_to have_content "feature" - end - end - - context 'filter by label enhancement and bug in issues list' do - before do - select_labels('bug', 'enhancement') - end - - it 'applies the filters' do - expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) - expect(page).to have_content "Bugfix2" - expect(page).not_to have_content "Feature1" - expect(find('.filtered-labels')).to have_content "bug" - expect(find('.filtered-labels')).to have_content "enhancement" - expect(find('.filtered-labels')).not_to have_content "feature" - - find('.js-label-filter-remove', match: :first).click - wait_for_ajax - - expect(page).to have_content "Bugfix2" - expect(page).not_to have_content "Feature1" - expect(page).not_to have_content "Bugfix1" - expect(find('.filtered-labels')).not_to have_content "bug" - expect(find('.filtered-labels')).to have_content "enhancement" - expect(find('.filtered-labels')).not_to have_content "feature" - end - end - - context 'remove filtered labels' do - before do - page.within '.labels-filter' do - click_button 'Label' - wait_for_ajax - click_link 'bug' - find('.dropdown-menu-close').click - end - - page.within '.filtered-labels' do - expect(page).to have_content 'bug' - end - end - - it 'allows user to remove filtered labels' do - first('.js-label-filter-remove').click - wait_for_ajax - - expect(find('.filtered-labels', visible: false)).not_to have_content 'bug' - expect(find('.labels-filter')).not_to have_content 'bug' - end - end - - context 'dropdown filtering' do - it 'filters by label name' do - page.within '.labels-filter' do - click_button 'Label' - wait_for_ajax - find('.dropdown-input input').set 'bug' - - page.within '.dropdown-content' do - expect(page).not_to have_content 'enhancement' - expect(page).to have_content 'bug' - end - end - end - end - - def select_labels(*labels) - page.find('.js-label-select').click - wait_for_ajax - labels.each do |label| - execute_script("$('.dropdown-menu-labels li:contains(\"#{label}\") a').click()") - end - page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click - wait_for_ajax - end -end diff --git a/spec/features/merge_requests/filter_by_labels_spec.rb b/spec/features/merge_requests/filter_by_labels_spec.rb new file mode 100644 index 00000000000..eff350ed53a --- /dev/null +++ b/spec/features/merge_requests/filter_by_labels_spec.rb @@ -0,0 +1,154 @@ +require 'rails_helper' + +feature 'Issue filtering by Labels', feature: true, js: true do + include WaitForAjax + + let(:project) { create(:project, :public) } + let!(:user) { create(:user) } + let!(:label) { create(:label, project: project) } + + let!(:bug) { create(:label, project: project, title: 'bug') } + let!(:feature) { create(:label, project: project, title: 'feature') } + let!(:enhancement) { create(:label, project: project, title: 'enhancement') } + + let!(:mr1) { create(:merge_request, title: "Bugfix1", source_project: project, target_project: project, source_branch: "bugfix1") } + let!(:mr2) { create(:merge_request, title:"Bugfix2", source_project: project, target_project: project, source_branch: "bugfix2") } + let!(:mr3) { create(:merge_request, title: "Feature1", source_project: project, target_project: project, source_branch: "feature1") } + + before do + mr1.labels << bug + + mr2.labels << bug + mr2.labels << enhancement + + mr3.title = "Feature1" + mr3.labels << feature + + project.team << [user, :master] + login_as(user) + + visit namespace_project_merge_requests_path(project.namespace, project) + end + + context 'filter by label bug' do + before do + select_labels('bug') + end + + it 'apply the filter' do + expect(page).to have_content "Bugfix1" + expect(page).to have_content "Bugfix2" + expect(page).not_to have_content "Feature1" + expect(find('.filtered-labels')).to have_content "bug" + expect(find('.filtered-labels')).not_to have_content "feature" + expect(find('.filtered-labels')).not_to have_content "enhancement" + + find('.js-label-filter-remove').click + wait_for_ajax + expect(find('.filtered-labels', visible: false)).to have_no_content "bug" + end + end + + context 'filter by label feature' do + before do + select_labels('feature') + end + + it 'applies the filter' do + expect(page).to have_content "Feature1" + expect(page).not_to have_content "Bugfix2" + expect(page).not_to have_content "Bugfix1" + expect(find('.filtered-labels')).to have_content "feature" + expect(find('.filtered-labels')).not_to have_content "bug" + expect(find('.filtered-labels')).not_to have_content "enhancement" + end + end + + context 'filter by label enhancement' do + before do + select_labels('enhancement') + end + + it 'applies the filter' do + expect(page).to have_content "Bugfix2" + expect(page).not_to have_content "Feature1" + expect(page).not_to have_content "Bugfix1" + expect(find('.filtered-labels')).to have_content "enhancement" + expect(find('.filtered-labels')).not_to have_content "bug" + expect(find('.filtered-labels')).not_to have_content "feature" + end + end + + context 'filter by label enhancement and bug in issues list' do + before do + select_labels('bug', 'enhancement') + end + + it 'applies the filters' do + expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) + expect(page).to have_content "Bugfix2" + expect(page).not_to have_content "Feature1" + expect(find('.filtered-labels')).to have_content "bug" + expect(find('.filtered-labels')).to have_content "enhancement" + expect(find('.filtered-labels')).not_to have_content "feature" + + find('.js-label-filter-remove', match: :first).click + wait_for_ajax + + expect(page).to have_content "Bugfix2" + expect(page).not_to have_content "Feature1" + expect(page).not_to have_content "Bugfix1" + expect(find('.filtered-labels')).not_to have_content "bug" + expect(find('.filtered-labels')).to have_content "enhancement" + expect(find('.filtered-labels')).not_to have_content "feature" + end + end + + context 'remove filtered labels' do + before do + page.within '.labels-filter' do + click_button 'Label' + wait_for_ajax + click_link 'bug' + find('.dropdown-menu-close').click + end + + page.within '.filtered-labels' do + expect(page).to have_content 'bug' + end + end + + it 'allows user to remove filtered labels' do + first('.js-label-filter-remove').click + wait_for_ajax + + expect(find('.filtered-labels', visible: false)).not_to have_content 'bug' + expect(find('.labels-filter')).not_to have_content 'bug' + end + end + + context 'dropdown filtering' do + it 'filters by label name' do + page.within '.labels-filter' do + click_button 'Label' + wait_for_ajax + find('.dropdown-input input').set 'bug' + + page.within '.dropdown-content' do + expect(page).not_to have_content 'enhancement' + expect(page).to have_content 'bug' + end + end + end + end + + def select_labels(*labels) + page.find('.js-label-select').click + wait_for_ajax + labels.each do |label| + execute_script("$('.dropdown-menu-labels li:contains(\"#{label}\") a').click()") + end + page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click + wait_for_ajax + end +end -- cgit v1.2.1 From 1623259ce70f39feca0db0924d65868fb7c773ff Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 11 Nov 2016 14:53:12 -0600 Subject: Remove spec since it already exists in MR page --- spec/features/issues/filter_by_milestone_spec.rb | 91 ------------------------ 1 file changed, 91 deletions(-) delete mode 100644 spec/features/issues/filter_by_milestone_spec.rb diff --git a/spec/features/issues/filter_by_milestone_spec.rb b/spec/features/issues/filter_by_milestone_spec.rb deleted file mode 100644 index 9dfa5d1de19..00000000000 --- a/spec/features/issues/filter_by_milestone_spec.rb +++ /dev/null @@ -1,91 +0,0 @@ -require 'rails_helper' - -feature 'Issue filtering by Milestone', feature: true do - let(:project) { create(:project, :public) } - let(:milestone) { create(:milestone, project: project) } - - scenario 'filters by no Milestone', js: true do - create(:issue, project: project) - create(:issue, project: project, milestone: milestone) - - visit_issues(project) - filter_by_milestone(Milestone::None.title) - - expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'No Milestone') - expect(page).to have_css('.issue', count: 1) - end - - context 'filters by upcoming milestone', js: true do - it 'does not show issues with no expiry' do - create(:issue, project: project) - create(:issue, project: project, milestone: milestone) - - visit_issues(project) - filter_by_milestone(Milestone::Upcoming.title) - - expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming') - expect(page).to have_css('.issue', count: 0) - end - - it 'shows issues in future' do - milestone = create(:milestone, project: project, due_date: Date.tomorrow) - create(:issue, project: project) - create(:issue, project: project, milestone: milestone) - - visit_issues(project) - filter_by_milestone(Milestone::Upcoming.title) - - expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming') - expect(page).to have_css('.issue', count: 1) - end - - it 'does not show issues in past' do - milestone = create(:milestone, project: project, due_date: Date.yesterday) - create(:issue, project: project) - create(:issue, project: project, milestone: milestone) - - visit_issues(project) - filter_by_milestone(Milestone::Upcoming.title) - - expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming') - expect(page).to have_css('.issue', count: 0) - end - end - - scenario 'filters by a specific Milestone', js: true do - create(:issue, project: project, milestone: milestone) - create(:issue, project: project) - - visit_issues(project) - filter_by_milestone(milestone.title) - - expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: milestone.title) - expect(page).to have_css('.issue', count: 1) - end - - context 'when milestone has single quotes in title' do - background do - milestone.update(name: "rock 'n' roll") - end - - scenario 'filters by a specific Milestone', js: true do - create(:issue, project: project, milestone: milestone) - create(:issue, project: project) - - visit_issues(project) - filter_by_milestone(milestone.title) - - expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: milestone.title) - expect(page).to have_css('.issue', count: 1) - end - end - - def visit_issues(project) - visit namespace_project_issues_path(project.namespace, project) - end - - def filter_by_milestone(title) - find(".js-milestone-select").click - find(".milestone-filter .dropdown-content a", text: title).click - end -end -- cgit v1.2.1 From e058ce9f1b98cb7e2ba4873c78eee79ed191806c Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 11 Nov 2016 15:10:13 -0600 Subject: Convert and move reset filters spec to MR --- spec/features/issues/reset_filters_spec.rb | 89 -------------------- spec/features/merge_requests/reset_filters_spec.rb | 96 ++++++++++++++++++++++ 2 files changed, 96 insertions(+), 89 deletions(-) delete mode 100644 spec/features/issues/reset_filters_spec.rb create mode 100644 spec/features/merge_requests/reset_filters_spec.rb diff --git a/spec/features/issues/reset_filters_spec.rb b/spec/features/issues/reset_filters_spec.rb deleted file mode 100644 index c9a3ecf16ea..00000000000 --- a/spec/features/issues/reset_filters_spec.rb +++ /dev/null @@ -1,89 +0,0 @@ -require 'rails_helper' - -feature 'Issues filter reset button', feature: true, js: true do - include WaitForAjax - include IssueHelpers - - let!(:project) { create(:project, :public) } - let!(:user) { create(:user)} - let!(:milestone) { create(:milestone, project: project) } - let!(:bug) { create(:label, project: project, name: 'bug')} - let!(:issue1) { create(:issue, project: project, milestone: milestone, author: user, assignee: user, title: 'Feature')} - let!(:issue2) { create(:labeled_issue, project: project, labels: [bug], title: 'Bugfix1')} - - before do - project.team << [user, :developer] - end - - context 'when a milestone filter has been applied' do - it 'resets the milestone filter' do - visit_issues(project, milestone_title: milestone.title) - expect(page).to have_css('.issue', count: 1) - - reset_filters - expect(page).to have_css('.issue', count: 2) - end - end - - context 'when a label filter has been applied' do - it 'resets the label filter' do - visit_issues(project, label_name: bug.name) - expect(page).to have_css('.issue', count: 1) - - reset_filters - expect(page).to have_css('.issue', count: 2) - end - end - - context 'when a text search has been conducted' do - it 'resets the text search filter' do - visit_issues(project, search: 'Bug') - expect(page).to have_css('.issue', count: 1) - - reset_filters - expect(page).to have_css('.issue', count: 2) - end - end - - context 'when author filter has been applied' do - it 'resets the author filter' do - visit_issues(project, author_id: user.id) - expect(page).to have_css('.issue', count: 1) - - reset_filters - expect(page).to have_css('.issue', count: 2) - end - end - - context 'when assignee filter has been applied' do - it 'resets the assignee filter' do - visit_issues(project, assignee_id: user.id) - expect(page).to have_css('.issue', count: 1) - - reset_filters - expect(page).to have_css('.issue', count: 2) - end - end - - context 'when all filters have been applied' do - it 'resets all filters' do - visit_issues(project, assignee_id: user.id, author_id: user.id, milestone_title: milestone.title, label_name: bug.name, search: 'Bug') - expect(page).to have_css('.issue', count: 0) - - reset_filters - expect(page).to have_css('.issue', count: 2) - end - end - - context 'when no filters have been applied' do - it 'the reset link should not be visible' do - visit_issues(project) - expect(page).to have_css('.issue', count: 2) - expect(page).not_to have_css '.reset_filters' - end - end - - def reset_filters - find('.reset-filters').click - end -end diff --git a/spec/features/merge_requests/reset_filters_spec.rb b/spec/features/merge_requests/reset_filters_spec.rb new file mode 100644 index 00000000000..2e468f2edf3 --- /dev/null +++ b/spec/features/merge_requests/reset_filters_spec.rb @@ -0,0 +1,96 @@ +require 'rails_helper' + +feature 'Issues filter reset button', feature: true, js: true do + include WaitForAjax + include IssueHelpers + + let!(:project) { create(:project, :public) } + let!(:user) { create(:user)} + let!(:milestone) { create(:milestone, project: project) } + let!(:bug) { create(:label, project: project, name: 'bug')} + let!(:mr1) { create(:merge_request, title: "Feature", source_project: project, target_project: project, source_branch: "Feature", milestone: milestone, author: user, assignee: user) } + let!(:mr2) { create(:merge_request, title:"Bugfix1", source_project: project, target_project: project, source_branch: "Bugfix1") } + + let(:merge_request_css) { '.merge-request' } + + before do + mr2.labels << bug + project.team << [user, :developer] + end + + context 'when a milestone filter has been applied' do + it 'resets the milestone filter' do + visit_merge_requests(project, milestone_title: milestone.title) + expect(page).to have_css(merge_request_css, count: 1) + + reset_filters + expect(page).to have_css(merge_request_css, count: 2) + end + end + + context 'when a label filter has been applied' do + it 'resets the label filter' do + visit_merge_requests(project, label_name: bug.name) + expect(page).to have_css(merge_request_css, count: 1) + + reset_filters + expect(page).to have_css(merge_request_css, count: 2) + end + end + + context 'when a text search has been conducted' do + it 'resets the text search filter' do + visit_merge_requests(project, search: 'Bug') + expect(page).to have_css(merge_request_css, count: 1) + + reset_filters + expect(page).to have_css(merge_request_css, count: 2) + end + end + + context 'when author filter has been applied' do + it 'resets the author filter' do + visit_merge_requests(project, author_id: user.id) + expect(page).to have_css(merge_request_css, count: 1) + + reset_filters + expect(page).to have_css(merge_request_css, count: 2) + end + end + + context 'when assignee filter has been applied' do + it 'resets the assignee filter' do + visit_merge_requests(project, assignee_id: user.id) + expect(page).to have_css(merge_request_css, count: 1) + + reset_filters + expect(page).to have_css(merge_request_css, count: 2) + end + end + + context 'when all filters have been applied' do + it 'resets all filters' do + visit_merge_requests(project, assignee_id: user.id, author_id: user.id, milestone_title: milestone.title, label_name: bug.name, search: 'Bug') + expect(page).to have_css(merge_request_css, count: 0) + + reset_filters + expect(page).to have_css(merge_request_css, count: 2) + end + end + + context 'when no filters have been applied' do + it 'the reset link should not be visible' do + visit_merge_requests(project) + expect(page).to have_css(merge_request_css, count: 2) + expect(page).not_to have_css '.reset_filters' + end + end + + def visit_merge_requests(project, opts = {}) + visit namespace_project_merge_requests_path project.namespace, project, opts + end + + def reset_filters + find('.reset-filters').click + end +end -- cgit v1.2.1 From e4c233224552484544b764a35a4686a6be3c9dea Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 11 Nov 2016 15:42:40 -0600 Subject: Add filter by merge request spec based on previous filter by issues spec --- .../merge_requests/filter_merge_requests_spec.rb | 355 +++++++++++++++++++++ 1 file changed, 355 insertions(+) create mode 100644 spec/features/merge_requests/filter_merge_requests_spec.rb diff --git a/spec/features/merge_requests/filter_merge_requests_spec.rb b/spec/features/merge_requests/filter_merge_requests_spec.rb new file mode 100644 index 00000000000..4642b5a530d --- /dev/null +++ b/spec/features/merge_requests/filter_merge_requests_spec.rb @@ -0,0 +1,355 @@ +require 'rails_helper' + +describe 'Filter merge requests', feature: true do + include WaitForAjax + + let!(:project) { create(:project) } + let!(:group) { create(:group) } + let!(:user) { create(:user)} + let!(:milestone) { create(:milestone, project: project) } + let!(:label) { create(:label, project: project) } + let!(:wontfix) { create(:label, project: project, title: "Won't fix") } + + before do + project.team << [user, :master] + group.add_developer(user) + login_as(user) + create(:merge_request, source_project: project, target_project: project) + end + + describe 'for assignee from mr#index' do + before do + visit namespace_project_merge_requests_path(project.namespace, project) + + find('.js-assignee-search').click + + find('.dropdown-menu-user-link', text: user.username).click + + wait_for_ajax + end + + context 'assignee', js: true do + it 'updates to current user' do + expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + end + + it 'does not change when closed link is clicked' do + find('.issues-state-filters a', text: "Closed").click + + expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + end + + it 'does not change when all link is clicked' do + find('.issues-state-filters a', text: "All").click + + expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + end + end + end + + describe 'for milestone from mr#index' do + before do + visit namespace_project_merge_requests_path(project.namespace, project) + + find('.js-milestone-select').click + + find('.milestone-filter .dropdown-content a', text: milestone.title).click + + wait_for_ajax + end + + context 'milestone', js: true do + it 'updates to current milestone' do + expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title) + end + + it 'does not change when closed link is clicked' do + find('.issues-state-filters a', text: "Closed").click + + expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title) + end + + it 'does not change when all link is clicked' do + find('.issues-state-filters a', text: "All").click + + expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title) + end + end + end + + describe 'for label from mr#index', js: true do + before do + visit namespace_project_merge_requests_path(project.namespace, project) + find('.js-label-select').click + wait_for_ajax + end + + it 'filters by any label' do + find('.dropdown-menu-labels a', text: 'Any Label').click + page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click + wait_for_ajax + + expect(find('.labels-filter')).to have_content 'Label' + end + + it 'filters by no label' do + find('.dropdown-menu-labels a', text: 'No Label').click + page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click + wait_for_ajax + + page.within '.labels-filter' do + expect(page).to have_content 'Labels' + end + expect(find('.js-label-select .dropdown-toggle-text')).to have_content('Labels') + end + + it 'filters by a label' do + find('.dropdown-menu-labels a', text: label.title).click + page.within '.labels-filter' do + expect(page).to have_content label.title + end + expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title) + end + + it "filters by `won't fix` and another label" do + page.within '.labels-filter' do + click_link wontfix.title + expect(page).to have_content wontfix.title + click_link label.title + end + + expect(find('.js-label-select .dropdown-toggle-text')).to have_content("#{wontfix.title} +1 more") + end + + it "filters by `won't fix` label followed by another label after page load" do + page.within '.labels-filter' do + click_link wontfix.title + expect(page).to have_content wontfix.title + end + + find('body').click + + expect(find('.filtered-labels')).to have_content(wontfix.title) + + find('.js-label-select').click + wait_for_ajax + find('.dropdown-menu-labels a', text: label.title).click + + find('body').click + + expect(find('.filtered-labels')).to have_content(wontfix.title) + expect(find('.filtered-labels')).to have_content(label.title) + + find('.js-label-select').click + wait_for_ajax + + expect(find('.dropdown-menu-labels li', text: wontfix.title)).to have_css('.is-active') + expect(find('.dropdown-menu-labels li', text: label.title)).to have_css('.is-active') + end + + it "selects and unselects `won't fix`" do + find('.dropdown-menu-labels a', text: wontfix.title).click + find('.dropdown-menu-labels a', text: wontfix.title).click + # Close label dropdown to load + find('body').click + expect(page).not_to have_css('.filtered-labels') + end + end + + describe 'for assignee and label from issues#index' do + before do + visit namespace_project_merge_requests_path(project.namespace, project) + + find('.js-assignee-search').click + + find('.dropdown-menu-user-link', text: user.username).click + + expect(page).not_to have_selector('.mr-list .merge-request') + + find('.js-label-select').click + + find('.dropdown-menu-labels .dropdown-content a', text: label.title).click + page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click + + wait_for_ajax + end + + context 'assignee and label', js: true do + it 'updates to current assignee and label' do + expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title) + end + + it 'does not change when closed link is clicked' do + find('.issues-state-filters a', text: "Closed").click + + expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title) + end + + it 'does not change when all link is clicked' do + find('.issues-state-filters a', text: "All").click + + expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title) + end + end + end + + describe 'filter merge requests by text' do + before do + create(:merge_request, title: "Bug", source_project: project, target_project: project, source_branch: "bug") + + bug_label = create(:label, project: project, title: 'bug') + milestone = create(:milestone, title: "8", project: project) + + mr = create(:merge_request, + title: "Bug 2", + source_project: project, + target_project: project, + source_branch: "bug2", + milestone: milestone, + author: user, + assignee: user) + mr.labels << bug_label + + visit namespace_project_merge_requests_path(project.namespace, project) + end + + context 'only text', js: true do + it 'filters merge requests by searched text' do + fill_in 'issuable_search', with: 'Bug' + + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 2) + end + end + + it 'does not show any merge requests' do + fill_in 'issuable_search', with: 'testing' + + page.within '.mr-list' do + expect(page).not_to have_selector('.merge-request') + end + end + end + + context 'text and dropdown options', js: true do + it 'filters by text and label' do + fill_in 'issuable_search', with: 'Bug' + + expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 2) + end + + click_button 'Label' + page.within '.labels-filter' do + click_link 'bug' + end + find('.dropdown-menu-close-icon').click + + expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 1) + end + end + + it 'filters by text and milestone' do + fill_in 'issuable_search', with: 'Bug' + + expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 2) + end + + click_button 'Milestone' + page.within '.milestone-filter' do + click_link '8' + end + + expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 1) + end + end + + it 'filters by text and assignee' do + fill_in 'issuable_search', with: 'Bug' + + expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 2) + end + + click_button 'Assignee' + page.within '.dropdown-menu-assignee' do + click_link user.name + end + + expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 1) + end + end + + it 'filters by text and author' do + fill_in 'issuable_search', with: 'Bug' + + expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 2) + end + + click_button 'Author' + page.within '.dropdown-menu-author' do + click_link user.name + end + + expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 1) + end + end + end + end + + describe 'filter merge requests and sort', js: true do + before do + bug_label = create(:label, project: project, title: 'bug') + + mr1 = create(:merge_request, title: "Frontend", source_project: project, target_project: project, source_branch: "Frontend") + mr2 = create(:merge_request, title: "Bug 2", source_project: project, target_project: project, source_branch: "bug2") + + mr1.labels << bug_label + mr2.labels << bug_label + + visit namespace_project_merge_requests_path(project.namespace, project) + end + + it 'is able to filter and sort merge requests' do + click_button 'Label' + wait_for_ajax + page.within '.labels-filter' do + click_link 'bug' + end + find('.dropdown-menu-close-icon').click + wait_for_ajax + + expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 2) + end + + click_button 'Last created' + page.within '.dropdown-menu-sort' do + click_link 'Oldest created' + end + wait_for_ajax + + page.within '.mr-list' do + expect(page).to have_content('Frontend') + end + end + end +end -- cgit v1.2.1 From 7f4609198d2a46e8b850a535cb17a3f67d87e25e Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 14 Nov 2016 10:25:32 -0600 Subject: Added more specs --- spec/features/issues/filter_issues_spec.rb | 72 ++++++++++++++++++++++ .../merge_requests/filter_by_labels_spec.rb | 2 +- 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index 7d681742045..2f8e7adad89 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -15,6 +15,7 @@ describe 'Filter issues', feature: true do let!(:bug_label) { create(:label, project: project, title: 'bug') } let!(:caps_sensitive_label) { create(:label, project: project, title: 'CAPS_sensitive') } let!(:milestone) { create(:milestone, title: "8", project: project) } + let!(:multiple_words_label) { create(:label, project: project, title: "Two words") } def input_filtered_search(search_term) filtered_search = find('.filtered-search') @@ -78,6 +79,9 @@ describe 'Filter issues', feature: true do issue_with_everything.labels << bug_label issue_with_everything.labels << caps_sensitive_label + multiple_words_label_issue = create(:issue, title: "Issue with multiple words label", project: project) + multiple_words_label_issue.labels << multiple_words_label + visit namespace_project_issues_path(project.namespace, project) end @@ -190,6 +194,61 @@ describe 'Filter issues', feature: true do input_filtered_search("label:#{bug_label.title} label:#{caps_sensitive_label.title}") expect_issues_list_count(1) end + + it 'filters issues by label containing special characters' do + special_label = create(:label, project: project, title: '!@#{$%^&*()-+[]<>?/:{}|\}') + special_issue = create(:issue, title: "Issue with special character label", project: project) + special_issue.labels << special_label + input_filtered_search("label:#{special_label.title}") + expect_issues_list_count(1) + end + + it 'does not show issues' do + new_label = create(:label, project: project, title: "new_label") + input_filtered_search("label:#{new_label.title}") + expect_no_issues_list() + end + end + + context 'label with multiple words', js: true do + it 'special characters' do + special_multiple_label = create(:label, project: project, title: "Utmost |mp0rt@nce") + special_multiple_issue = create(:issue, title: "Issue with special character multiple words label", project: project) + special_multiple_issue.labels << special_multiple_label + + input_filtered_search("label:'#{special_multiple_label.title}'") + expect_issues_list_count(1) + end + + it 'single quotes' do + input_filtered_search("label:'#{multiple_words_label.title}'") + expect_issues_list_count(1) + end + + it 'double quotes' do + input_filtered_search("label:\"#{multiple_words_label.title}\"") + expect_issues_list_count(1) + end + + it 'single quotes containing double quotes' do + # TODO: Actual bug + + # double_quotes_label = create(:label, project: project, title: 'won"t fix') + # double_quotes_label_issue = create(:issue, title: "Issue with double quotes label", project: project) + # double_quotes_label_issue.labels << double_quotes_label + + # input_filtered_search("label:'#{double_quotes_label.title}'") + # expect_issues_list_count(1) + end + + it 'double quotes containing single quotes' do + single_quotes_label = create(:label, project: project, title: "won't fix") + single_quotes_label_issue = create(:issue, title: "Issue with single quotes label", project: project) + single_quotes_label_issue.labels << single_quotes_label + + input_filtered_search("label:\"#{single_quotes_label.title}\"") + expect_issues_list_count(1) + end end context 'label with other filters', js: true do @@ -269,6 +328,19 @@ describe 'Filter issues', feature: true do it 'filters issues by multiple milestones' do # YOLO end + + it 'filters issues by milestone containing special characters' do + special_milestone = create(:milestone, title: '!@\#{$%^&*()}', project: project) + create(:issue, title: "Issue with special character milestone", project: project, milestone: special_milestone) + input_filtered_search('milestone:!@\#{$%^&*()}') + expect_issues_list_count(1) + end + + it 'does not show issues' do + new_milestone = create(:milestone, title: "new", project: project) + input_filtered_search("milestone:#{new_milestone}") + expect_no_issues_list() + end end context 'milestone with other filters', js: true do diff --git a/spec/features/merge_requests/filter_by_labels_spec.rb b/spec/features/merge_requests/filter_by_labels_spec.rb index eff350ed53a..4c60329865c 100644 --- a/spec/features/merge_requests/filter_by_labels_spec.rb +++ b/spec/features/merge_requests/filter_by_labels_spec.rb @@ -12,7 +12,7 @@ feature 'Issue filtering by Labels', feature: true, js: true do let!(:enhancement) { create(:label, project: project, title: 'enhancement') } let!(:mr1) { create(:merge_request, title: "Bugfix1", source_project: project, target_project: project, source_branch: "bugfix1") } - let!(:mr2) { create(:merge_request, title:"Bugfix2", source_project: project, target_project: project, source_branch: "bugfix2") } + let!(:mr2) { create(:merge_request, title: "Bugfix2", source_project: project, target_project: project, source_branch: "bugfix2") } let!(:mr3) { create(:merge_request, title: "Feature1", source_project: project, target_project: project, source_branch: "feature1") } before do -- cgit v1.2.1 From 01eb0571f0498225c3d75df419b8a50a47739dc8 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 14 Nov 2016 10:37:55 -0600 Subject: Resolve MR review suggestions --- .../filtered_search/filtered_search_bundle.js | 6 ----- .../filtered_search/filtered_search_manager.js.es6 | 28 +++++++++++----------- .../filtered_search/filtered_search_tokenizer.es6 | 2 +- 3 files changed, 15 insertions(+), 21 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_bundle.js b/app/assets/javascripts/filtered_search/filtered_search_bundle.js index 656979ba82f..d188718c5f3 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_bundle.js +++ b/app/assets/javascripts/filtered_search/filtered_search_bundle.js @@ -1,4 +1,3 @@ - /* eslint-disable */ // This is a manifest file that'll be compiled into including all the files listed below. // Add new JavaScript code in separate files in this directory and they'll automatically // be included in the compiled file accessible from http://example.com/assets/application.js @@ -6,8 +5,3 @@ // the compiled file. // /*= require_tree . */ - - (function() { - - }).call(this); - \ No newline at end of file diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index db414b9755d..26b9d334545 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -18,22 +18,22 @@ param: 'name[]', }]; - function clearSearch(event) { - event.stopPropagation(); - event.preventDefault(); + function clearSearch(e) { + e.stopPropagation(); + e.preventDefault(); document.querySelector('.filtered-search').value = ''; document.querySelector('.clear-search').classList.add('hidden'); } - function toggleClearSearchButton(event) { + function toggleClearSearchButton(e) { const clearSearchButton = document.querySelector('.clear-search'); if (event.target.value) { - clearSearchButton.classList.remove('hidden'); - } else { - clearSearchButton.classList.add('hidden'); - } + clearSearchButton.classList.remove('hidden'); + } else { + clearSearchButton.classList.add('hidden'); + } } function loadSearchParamsFromURL() { @@ -97,16 +97,16 @@ document.querySelector('.clear-search').addEventListener('click', clearSearch); } - processInput(event) { - const input = event.target.value; + processInput(e) { + const input = e.target.value; this.tokenizer.processTokens(input); } - checkForEnter(event) { + checkForEnter(e) { // Enter KeyCode - if (event.keyCode === 13) { - event.stopPropagation(); - event.preventDefault(); + if (e.keyCode === 13) { + e.stopPropagation(); + e.preventDefault(); this.search(); } } diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 index ddb173b2d98..de91081edfa 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 @@ -36,7 +36,7 @@ inputs.forEach((i) => { if (incompleteToken) { - const prevToken = this.tokens[this.tokens.length - 1]; + const prevToken = this.tokens.last(); prevToken.value += ` ${i}`; // Remove last quotation -- cgit v1.2.1 From 329b03b3c3fa51f365dee867cf4d8cef5ad23d4e Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 14 Nov 2016 13:22:32 -0600 Subject: Add token symbol matching --- .../filtered_search/filtered_search_manager.js.es6 | 94 ++++++++++++++++------ .../filtered_search/filtered_search_tokenizer.es6 | 10 ++- 2 files changed, 78 insertions(+), 26 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 26b9d334545..31e570bd6b6 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -4,18 +4,37 @@ key: 'author', type: 'string', param: 'username', + symbol: '@', }, { key: 'assignee', type: 'string', param: 'username', + symbol: '@', + conditions: [{ + keyword: 'none', + url: 'assignee_id=0', + }] }, { key: 'milestone', type: 'string', param: 'title', + symbol: '%', + conditions: [{ + keyword: 'none', + url: 'milestone_title=No+Milestone', + }, { + keyword: 'upcoming', + url: 'milestone_title=%23upcoming', + }] }, { key: 'label', type: 'array', param: 'name[]', + symbol: '~', + conditions: [{ + keyword: 'none', + url: 'label_name[]=No+Label', + }] }]; function clearSearch(e) { @@ -47,28 +66,42 @@ const key = decodeURIComponent(split[0]); const value = split[1]; - // Sanitize value since URL converts spaces into + - // Replace before decode so that we know what was originally + versus the encoded + - const sanitizedValue = value ? decodeURIComponent(value.replace(/[+]/g, ' ')) : value; - const match = validTokenKeys.filter(t => key === `${t.key}_${t.param}`)[0]; - - if (match) { - const sanitizedKey = key.slice(0, key.indexOf('_')); - const valueHasSpace = sanitizedValue.indexOf(' ') !== -1; - - const preferredQuotations = '"'; - let quotationsToUse = preferredQuotations; - - if (valueHasSpace) { - // Prefer ", but use ' if required - quotationsToUse = sanitizedValue.indexOf(preferredQuotations) === -1 ? preferredQuotations : '\''; + // Check if it matches edge conditions listed in validTokenKeys + let conditionIndex = 0; + const validCondition = validTokenKeys.filter(v => v.conditions && v.conditions.filter((c, index) => { + if (c.url === p) { + conditionIndex = index; + } + return c.url === p; + })[0])[0]; + + if (validCondition) { + inputValue += `${validCondition.key}:${validCondition.conditions[conditionIndex].keyword}`; + } else { + // Sanitize value since URL converts spaces into + + // Replace before decode so that we know what was originally + versus the encoded + + const sanitizedValue = value ? decodeURIComponent(value.replace(/[+]/g, ' ')) : value; + const match = validTokenKeys.filter(t => key === `${t.key}_${t.param}`)[0]; + + if (match) { + const sanitizedKey = key.slice(0, key.indexOf('_')); + const valueHasSpace = sanitizedValue.indexOf(' ') !== -1; + const symbol = match.symbol; + + const preferredQuotations = '"'; + let quotationsToUse = preferredQuotations; + + if (valueHasSpace) { + // Prefer ", but use ' if required + quotationsToUse = sanitizedValue.indexOf(preferredQuotations) === -1 ? preferredQuotations : '\''; + } + + inputValue += valueHasSpace ? `${sanitizedKey}:${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}` : `${sanitizedKey}:${symbol}${sanitizedValue}`; + inputValue += ' '; + } else if (!match && key === 'search') { + inputValue += sanitizedValue; + inputValue += ' '; } - - inputValue += valueHasSpace ? `${sanitizedKey}:${quotationsToUse}${sanitizedValue}${quotationsToUse}` : `${sanitizedKey}:${sanitizedValue}`; - inputValue += ' '; - } else if (!match && key === 'search') { - inputValue += sanitizedValue; - inputValue += ' '; } }); @@ -133,8 +166,23 @@ path += `&state=${currentState}`; tokens.forEach((token) => { - const param = validTokenKeys.filter(t => t.key === token.key)[0].param; - path += `&${token.key}_${param}=${encodeURIComponent(token.value)}`; + const match = validTokenKeys.filter(t => t.key === token.key)[0]; + let tokenPath = ''; + + if (token.wildcard && match.conditions) { + const condition = match.conditions.filter(c => c.keyword === token.value.toLowerCase())[0]; + + if (condition) { + tokenPath = `${condition.url}`; + } + } else if (!token.wildcard) { + // Remove the wildcard token + tokenPath = `${token.key}_${match.param}=${encodeURIComponent(token.value.slice(1))}`; + } else { + tokenPath = `${token.key}_${match.param}=${encodeURIComponent(token.value)}`; + } + + path += `&${tokenPath}`; }); if (searchToken) { diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 index de91081edfa..c3e5e817c9e 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 @@ -57,7 +57,10 @@ if (colonIndex !== -1) { const tokenKey = i.slice(0, colonIndex).toLowerCase(); const tokenValue = i.slice(colonIndex + 1); - const match = this.validTokenKeys.filter(v => v.key === tokenKey)[0]; + const tokenSymbol = tokenValue[0]; + console.log(tokenSymbol) + const keyMatch = this.validTokenKeys.filter(v => v.key === tokenKey)[0]; + const symbolMatch = this.validTokenKeys.filter(v => v.symbol === tokenSymbol)[0]; if (tokenValue.indexOf('"') !== -1) { lastQuotation = '"'; @@ -67,10 +70,11 @@ incompleteToken = true; } - if (match && tokenValue.length > 0) { + if (keyMatch && tokenValue.length > 0) { this.tokens.push({ - key: match.key, + key: keyMatch.key, value: tokenValue, + wildcard: symbolMatch ? false : true, }); return; -- cgit v1.2.1 From c07a522743443cbf7afe1ce85d014a1b9f41e8d8 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 14 Nov 2016 14:43:09 -0600 Subject: Update tests to include token symbol --- spec/features/issues/filter_issues_spec.rb | 178 +++++++++++++++-------------- 1 file changed, 95 insertions(+), 83 deletions(-) diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index 2f8e7adad89..c790f350b2d 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -82,13 +82,19 @@ describe 'Filter issues', feature: true do multiple_words_label_issue = create(:issue, title: "Issue with multiple words label", project: project) multiple_words_label_issue.labels << multiple_words_label + future_milestone = create(:milestone, title: "future", project: project, due_date: Time.now + 1.month) + issue_with_future_milestone = create(:issue, + title: "Issue with future milestone", + milestone: future_milestone, + project: project) + visit namespace_project_issues_path(project.namespace, project) end describe 'filter issues by author' do context 'only author', js: true do it 'filters issues by searched author' do - input_filtered_search("author:#{user.username}") + input_filtered_search("author:@#{user.username}") expect_issues_list_count(5) end @@ -103,22 +109,22 @@ describe 'Filter issues', feature: true do context 'author with other filters', js: true do it 'filters issues by searched author and text' do - input_filtered_search("author:#{user.username} issue") + input_filtered_search("author:@#{user.username} issue") expect_issues_list_count(3) end it 'filters issues by searched author, assignee and text' do - input_filtered_search("author:#{user.username} assignee:#{user.username} issue") + input_filtered_search("author:@#{user.username} assignee:@#{user.username} issue") expect_issues_list_count(3) end it 'filters issues by searched author, assignee, label, and text' do - input_filtered_search("author:#{user.username} assignee:#{user.username} label:#{caps_sensitive_label.title} issue") + input_filtered_search("author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} issue") expect_issues_list_count(1) end it 'filters issues by searched author, assignee, label, milestone and text' do - input_filtered_search("author:#{user.username} assignee:#{user.username} label:#{caps_sensitive_label.title} milestone:#{milestone.title} issue") + input_filtered_search("author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} issue") expect_issues_list_count(1) end end @@ -131,12 +137,13 @@ describe 'Filter issues', feature: true do describe 'filter issues by assignee' do context 'only assignee', js: true do it 'filters issues by searched assignee' do - input_filtered_search("assignee:#{user.username}") + input_filtered_search("assignee:@#{user.username}") expect_issues_list_count(5) end it 'filters issues by no assignee' do - # TODO + input_filtered_search("assignee:none") + expect_issues_list_count(8) end it 'filters issues by invalid assignee' do @@ -148,27 +155,27 @@ describe 'Filter issues', feature: true do end end - context 'assignee with other filters', js: true do - it 'filters issues by searched assignee and text' do - input_filtered_search("assignee:#{user.username} searchTerm") - expect_issues_list_count(2) - end + # context 'assignee with other filters', js: true do + # it 'filters issues by searched assignee and text' do + # input_filtered_search("assignee:@#{user.username} searchTerm") + # expect_issues_list_count(2) + # end - it 'filters issues by searched assignee, author and text' do - input_filtered_search("assignee:#{user.username} author:#{user.username} searchTerm") - expect_issues_list_count(2) - end + # it 'filters issues by searched assignee, author and text' do + # input_filtered_search("assignee:@#{user.username} author:@#{user.username} searchTerm") + # expect_issues_list_count(2) + # end - it 'filters issues by searched assignee, author, label, text' do - input_filtered_search("assignee:#{user.username} author:#{user.username} label:#{caps_sensitive_label.title} searchTerm") - expect_issues_list_count(1) - end + # it 'filters issues by searched assignee, author, label, text' do + # input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} searchTerm") + # expect_issues_list_count(1) + # end - it 'filters issues by searched assignee, author, label, milestone and text' do - input_filtered_search("assignee:#{user.username} author:#{user.username} label:#{caps_sensitive_label.title} milestone:#{milestone.title} searchTerm") - expect_issues_list_count(1) - end - end + # it 'filters issues by searched assignee, author, label, milestone and text' do + # input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} searchTerm") + # expect_issues_list_count(1) + # end + # end context 'sorting', js: true do # TODO @@ -178,12 +185,13 @@ describe 'Filter issues', feature: true do describe 'filter issues by label' do context 'only label', js: true do it 'filters issues by searched label' do - input_filtered_search("label:#{bug_label.title}") + input_filtered_search("label:~#{bug_label.title}") expect_issues_list_count(2) end it 'filters issues by no label' do - # TODO + input_filtered_search("label:none") + expect_issues_list_count(9) end it 'filters issues by invalid label' do @@ -191,7 +199,7 @@ describe 'Filter issues', feature: true do end it 'filters issues by multiple labels' do - input_filtered_search("label:#{bug_label.title} label:#{caps_sensitive_label.title}") + input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title}") expect_issues_list_count(1) end @@ -199,13 +207,13 @@ describe 'Filter issues', feature: true do special_label = create(:label, project: project, title: '!@#{$%^&*()-+[]<>?/:{}|\}') special_issue = create(:issue, title: "Issue with special character label", project: project) special_issue.labels << special_label - input_filtered_search("label:#{special_label.title}") + input_filtered_search("label:~#{special_label.title}") expect_issues_list_count(1) end it 'does not show issues' do new_label = create(:label, project: project, title: "new_label") - input_filtered_search("label:#{new_label.title}") + input_filtered_search("label:~#{new_label.title}") expect_no_issues_list() end end @@ -216,17 +224,17 @@ describe 'Filter issues', feature: true do special_multiple_issue = create(:issue, title: "Issue with special character multiple words label", project: project) special_multiple_issue.labels << special_multiple_label - input_filtered_search("label:'#{special_multiple_label.title}'") + input_filtered_search("label:~'#{special_multiple_label.title}'") expect_issues_list_count(1) end it 'single quotes' do - input_filtered_search("label:'#{multiple_words_label.title}'") + input_filtered_search("label:~'#{multiple_words_label.title}'") expect_issues_list_count(1) end it 'double quotes' do - input_filtered_search("label:\"#{multiple_words_label.title}\"") + input_filtered_search("label:~\"#{multiple_words_label.title}\"") expect_issues_list_count(1) end @@ -246,51 +254,51 @@ describe 'Filter issues', feature: true do single_quotes_label_issue = create(:issue, title: "Issue with single quotes label", project: project) single_quotes_label_issue.labels << single_quotes_label - input_filtered_search("label:\"#{single_quotes_label.title}\"") + input_filtered_search("label:~\"#{single_quotes_label.title}\"") expect_issues_list_count(1) end end context 'label with other filters', js: true do it 'filters issues by searched label and text' do - input_filtered_search("label:#{caps_sensitive_label.title} bug") + input_filtered_search("label:~#{caps_sensitive_label.title} bug") expect_issues_list_count(1) end it 'filters issues by searched label, author and text' do - input_filtered_search("label:#{caps_sensitive_label.title} author:#{user.username} bug") + input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} bug") expect_issues_list_count(1) end it 'filters issues by searched label, author, assignee and text' do - input_filtered_search("label:#{caps_sensitive_label.title} author:#{user.username} assignee:#{user.username} bug") + input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug") expect_issues_list_count(1) end it 'filters issues by searched label, author, assignee, milestone and text' do - input_filtered_search("label:#{caps_sensitive_label.title} author:#{user.username} assignee:#{user.username} milestone:#{milestone.title} bug") + input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug") expect_issues_list_count(1) end end context 'multiple labels with other filters', js: true do it 'filters issues by searched label, label2, and text' do - input_filtered_search("label:#{bug_label.title} label:#{caps_sensitive_label.title} bug") + input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} bug") expect_issues_list_count(1) end it 'filters issues by searched label, label2, author and text' do - input_filtered_search("label:#{bug_label.title} label:#{caps_sensitive_label.title} author:#{user.username} bug") + input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} bug") expect_issues_list_count(1) end it 'filters issues by searched label, label2, author, assignee and text' do - input_filtered_search("label:#{bug_label.title} label:#{caps_sensitive_label.title} author:#{user.username} assignee:#{user.username} bug") + input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug") expect_issues_list_count(1) end it 'filters issues by searched label, label2, author, assignee, milestone and text' do - input_filtered_search("label:#{bug_label.title} label:#{caps_sensitive_label.title} author:#{user.username} assignee:#{user.username} milestone:#{milestone.title} bug") + input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug") expect_issues_list_count(1) end end @@ -309,16 +317,18 @@ describe 'Filter issues', feature: true do describe 'filter issues by milestone' do context 'only milestone', js: true do it 'filters issues by searched milestone' do - input_filtered_search("milestone:#{milestone.title}") + input_filtered_search("milestone:%#{milestone.title}") expect_issues_list_count(5) end it 'filters issues by no milestone' do - # TODO + input_filtered_search("milestone:none") + expect_issues_list_count(7) end it 'filters issues by upcoming milestones' do - # TODO + input_filtered_search("milestone:upcoming") + expect_issues_list_count(1) end it 'filters issues by invalid milestones' do @@ -332,13 +342,13 @@ describe 'Filter issues', feature: true do it 'filters issues by milestone containing special characters' do special_milestone = create(:milestone, title: '!@\#{$%^&*()}', project: project) create(:issue, title: "Issue with special character milestone", project: project, milestone: special_milestone) - input_filtered_search('milestone:!@\#{$%^&*()}') + input_filtered_search('milestone:%!@\#{$%^&*()}') expect_issues_list_count(1) end it 'does not show issues' do new_milestone = create(:milestone, title: "new", project: project) - input_filtered_search("milestone:#{new_milestone}") + input_filtered_search("milestone:%#{new_milestone}") expect_no_issues_list() end end @@ -402,57 +412,57 @@ describe 'Filter issues', feature: true do context 'searched text with other filters', js: true do it 'filters issues by searched text and author' do - input_filtered_search("bug author:#{user.username}") + input_filtered_search("bug author:@#{user.username}") expect_issues_list_count(2) end it 'filters issues by searched text, author and more text' do - input_filtered_search("bug author:#{user.username} report") + input_filtered_search("bug author:@#{user.username} report") expect_issues_list_count(1) end it 'filters issues by searched text, author and assignee' do - input_filtered_search("bug author:#{user.username} assignee:#{user.username}") + input_filtered_search("bug author:@#{user.username} assignee:@#{user.username}") expect_issues_list_count(2) end it 'filters issues by searched text, author, more text and assignee' do - input_filtered_search("bug author:#{user.username} report assignee:#{user.username}") + input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username}") expect_issues_list_count(1) end it 'filters issues by searched text, author, more text, assignee and even more text' do - input_filtered_search("bug author:#{user.username} report assignee:#{user.username} with") + input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with") expect_issues_list_count(1) end it 'filters issues by searched text, author, assignee and label' do - input_filtered_search("bug author:#{user.username} assignee:#{user.username} label:#{bug_label.title}") + input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title}") expect_issues_list_count(2) end it 'filters issues by searched text, author, text, assignee, text, label and text' do - input_filtered_search("bug author:#{user.username} report assignee:#{user.username} with label:#{bug_label.title} everything") + input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything") expect_issues_list_count(1) end it 'filters issues by searched text, author, assignee, label and milestone' do - input_filtered_search("bug author:#{user.username} assignee:#{user.username} label:#{bug_label.title} milestone:#{milestone.title}") + input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title}") expect_issues_list_count(2) end it 'filters issues by searched text, author, text, assignee, text, label, text, milestone and text' do - input_filtered_search("bug author:#{user.username} report assignee:#{user.username} with label:#{bug_label.title} everything milestone:#{milestone.title} you") + input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything milestone:%#{milestone.title} you") expect_issues_list_count(1) end it 'filters issues by searched text, author, assignee, multiple labels and milestone' do - input_filtered_search("bug author:#{user.username} assignee:#{user.username} label:#{bug_label.title} label:#{caps_sensitive_label.title} milestone:#{milestone.title}") + input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title}") expect_issues_list_count(1) end it 'filters issues by searched text, author, text, assignee, text, label1, text, label2, text, milestone and text' do - input_filtered_search("bug author:#{user.username} report assignee:#{user.username} with label:#{bug_label.title} everything label:#{caps_sensitive_label.title} you milestone:#{milestone.title} thought") + input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything label:~#{caps_sensitive_label.title} you milestone:%#{milestone.title} thought") expect_issues_list_count(1) end end @@ -462,31 +472,33 @@ describe 'Filter issues', feature: true do end end - it 'updates atom feed link for project issues' do - visit namespace_project_issues_path(project.namespace, project, milestone_title: '', assignee_id: user.id) - link = find('.nav-controls a', text: 'Subscribe') - params = CGI::parse(URI.parse(link[:href]).query) - auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) - auto_discovery_params = CGI::parse(URI.parse(auto_discovery_link[:href]).query) - expect(params).to include('private_token' => [user.private_token]) - expect(params).to include('milestone_title' => ['']) - expect(params).to include('assignee_id' => [user.id.to_s]) - expect(auto_discovery_params).to include('private_token' => [user.private_token]) - expect(auto_discovery_params).to include('milestone_title' => ['']) - expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s]) - end + describe 'RSS feeds' do + it 'updates atom feed link for project issues' do + visit namespace_project_issues_path(project.namespace, project, milestone_title: '', assignee_id: user.id) + link = find('.nav-controls a', text: 'Subscribe') + params = CGI::parse(URI.parse(link[:href]).query) + auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) + auto_discovery_params = CGI::parse(URI.parse(auto_discovery_link[:href]).query) + expect(params).to include('private_token' => [user.private_token]) + expect(params).to include('milestone_title' => ['']) + expect(params).to include('assignee_id' => [user.id.to_s]) + expect(auto_discovery_params).to include('private_token' => [user.private_token]) + expect(auto_discovery_params).to include('milestone_title' => ['']) + expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s]) + end - it 'updates atom feed link for group issues' do - visit issues_group_path(group, milestone_title: '', assignee_id: user.id) - link = find('.nav-controls a', text: 'Subscribe') - params = CGI::parse(URI.parse(link[:href]).query) - auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) - auto_discovery_params = CGI::parse(URI.parse(auto_discovery_link[:href]).query) - expect(params).to include('private_token' => [user.private_token]) - expect(params).to include('milestone_title' => ['']) - expect(params).to include('assignee_id' => [user.id.to_s]) - expect(auto_discovery_params).to include('private_token' => [user.private_token]) - expect(auto_discovery_params).to include('milestone_title' => ['']) - expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s]) + it 'updates atom feed link for group issues' do + visit issues_group_path(group, milestone_title: '', assignee_id: user.id) + link = find('.nav-controls a', text: 'Subscribe') + params = CGI::parse(URI.parse(link[:href]).query) + auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) + auto_discovery_params = CGI::parse(URI.parse(auto_discovery_link[:href]).query) + expect(params).to include('private_token' => [user.private_token]) + expect(params).to include('milestone_title' => ['']) + expect(params).to include('assignee_id' => [user.id.to_s]) + expect(auto_discovery_params).to include('private_token' => [user.private_token]) + expect(auto_discovery_params).to include('milestone_title' => ['']) + expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s]) + end end end -- cgit v1.2.1 From e3395e3a9d683f62fb67e4d7b54381aad73e7a9e Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 14 Nov 2016 16:55:38 -0600 Subject: Add specs for clear search button --- spec/features/issues/search_bar_spec.rb | 55 +++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 spec/features/issues/search_bar_spec.rb diff --git a/spec/features/issues/search_bar_spec.rb b/spec/features/issues/search_bar_spec.rb new file mode 100644 index 00000000000..1d632671fe2 --- /dev/null +++ b/spec/features/issues/search_bar_spec.rb @@ -0,0 +1,55 @@ +require 'rails_helper' + +describe 'Search bar', feature: true do + include WaitForAjax + + let!(:project) { create(:project) } + let!(:group) { create(:group) } + let!(:user) { create(:user) } + + before do + project.team << [user, :master] + group.add_developer(user) + login_as(user) + create(:issue, project: project) + + visit namespace_project_issues_path(project.namespace, project) + end + + describe 'clear search button', js: true do + it 'clears text' do + search_text = 'search_text' + filtered_search = find('.filtered-search') + filtered_search.set(search_text) + + expect(filtered_search.value).to eq(search_text) + find('.filtered-search-input-container .clear-search').click + expect(filtered_search.value).to eq('') + end + + it 'hides by default' do + expect(page).to have_css('.clear-search', visible: false) + end + + it 'hides after clicked' do + filtered_search = find('.filtered-search') + filtered_search.set('a') + find('.filtered-search-input-container .clear-search').click + expect(page).to have_css('.clear-search', visible: false) + end + + it 'hides when there is no text' do + filtered_search = find('.filtered-search') + filtered_search.set('a') + filtered_search.set('') + expect(page).to have_css('.clear-search', visible: false) + end + + it 'shows when there is text' do + filtered_search = find('.filtered-search') + filtered_search.set('a') + + expect(page).to have_css('.clear-search', visible: true) + end + end +end -- cgit v1.2.1 From 8e3a52cfd68302ed75ffb89de3a08d1f70f876ad Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 14 Nov 2016 17:45:26 -0600 Subject: Fix eslint --- .../filtered_search/filtered_search_manager.js.es6 | 32 ++++++++++++---------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 31e570bd6b6..8568bf78416 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -13,7 +13,7 @@ conditions: [{ keyword: 'none', url: 'assignee_id=0', - }] + }], }, { key: 'milestone', type: 'string', @@ -25,7 +25,7 @@ }, { keyword: 'upcoming', url: 'milestone_title=%23upcoming', - }] + }], }, { key: 'label', type: 'array', @@ -34,7 +34,7 @@ conditions: [{ keyword: 'none', url: 'label_name[]=No+Label', - }] + }], }]; function clearSearch(e) { @@ -48,11 +48,11 @@ function toggleClearSearchButton(e) { const clearSearchButton = document.querySelector('.clear-search'); - if (event.target.value) { - clearSearchButton.classList.remove('hidden'); - } else { - clearSearchButton.classList.add('hidden'); - } + if (e.target.value) { + clearSearchButton.classList.remove('hidden'); + } else { + clearSearchButton.classList.add('hidden'); + } } function loadSearchParamsFromURL() { @@ -68,12 +68,13 @@ // Check if it matches edge conditions listed in validTokenKeys let conditionIndex = 0; - const validCondition = validTokenKeys.filter(v => v.conditions && v.conditions.filter((c, index) => { - if (c.url === p) { - conditionIndex = index; - } - return c.url === p; - })[0])[0]; + const validCondition = validTokenKeys + .filter(v => v.conditions && v.conditions.filter((c, index) => { + if (c.url === p) { + conditionIndex = index; + } + return c.url === p; + })[0])[0]; if (validCondition) { inputValue += `${validCondition.key}:${validCondition.conditions[conditionIndex].keyword}`; @@ -170,7 +171,8 @@ let tokenPath = ''; if (token.wildcard && match.conditions) { - const condition = match.conditions.filter(c => c.keyword === token.value.toLowerCase())[0]; + const condition = match.conditions + .filter(c => c.keyword === token.value.toLowerCase())[0]; if (condition) { tokenPath = `${condition.url}`; -- cgit v1.2.1 From 8be495318e6d477f98dd08de11261996faa050f2 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 14 Nov 2016 18:19:40 -0600 Subject: Add more specs --- spec/features/issues/filter_issues_spec.rb | 106 ++++++++++++++++----- spec/features/merge_requests/reset_filters_spec.rb | 2 +- 2 files changed, 83 insertions(+), 25 deletions(-) diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index c790f350b2d..0eed0ed4274 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -17,6 +17,8 @@ describe 'Filter issues', feature: true do let!(:milestone) { create(:milestone, title: "8", project: project) } let!(:multiple_words_label) { create(:label, project: project, title: "Two words") } + let!(:closed_issue) { create(:issue, title: 'bug that is closed', project: project, state: :closed) } + def input_filtered_search(search_term) filtered_search = find('.filtered-search') filtered_search.set(search_term) @@ -83,7 +85,8 @@ describe 'Filter issues', feature: true do multiple_words_label_issue.labels << multiple_words_label future_milestone = create(:milestone, title: "future", project: project, due_date: Time.now + 1.month) - issue_with_future_milestone = create(:issue, + + create(:issue, title: "Issue with future milestone", milestone: future_milestone, project: project) @@ -143,7 +146,7 @@ describe 'Filter issues', feature: true do it 'filters issues by no assignee' do input_filtered_search("assignee:none") - expect_issues_list_count(8) + expect_issues_list_count(8, 1) end it 'filters issues by invalid assignee' do @@ -155,27 +158,27 @@ describe 'Filter issues', feature: true do end end - # context 'assignee with other filters', js: true do - # it 'filters issues by searched assignee and text' do - # input_filtered_search("assignee:@#{user.username} searchTerm") - # expect_issues_list_count(2) - # end + context 'assignee with other filters', js: true do + it 'filters issues by searched assignee and text' do + input_filtered_search("assignee:@#{user.username} searchTerm") + expect_issues_list_count(2) + end - # it 'filters issues by searched assignee, author and text' do - # input_filtered_search("assignee:@#{user.username} author:@#{user.username} searchTerm") - # expect_issues_list_count(2) - # end + it 'filters issues by searched assignee, author and text' do + input_filtered_search("assignee:@#{user.username} author:@#{user.username} searchTerm") + expect_issues_list_count(2) + end - # it 'filters issues by searched assignee, author, label, text' do - # input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} searchTerm") - # expect_issues_list_count(1) - # end + it 'filters issues by searched assignee, author, label, text' do + input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} searchTerm") + expect_issues_list_count(1) + end - # it 'filters issues by searched assignee, author, label, milestone and text' do - # input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} searchTerm") - # expect_issues_list_count(1) - # end - # end + it 'filters issues by searched assignee, author, label, milestone and text' do + input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} searchTerm") + expect_issues_list_count(1) + end + end context 'sorting', js: true do # TODO @@ -191,7 +194,7 @@ describe 'Filter issues', feature: true do it 'filters issues by no label' do input_filtered_search("label:none") - expect_issues_list_count(9) + expect_issues_list_count(9, 1) end it 'filters issues by invalid label' do @@ -323,7 +326,7 @@ describe 'Filter issues', feature: true do it 'filters issues by no milestone' do input_filtered_search("milestone:none") - expect_issues_list_count(7) + expect_issues_list_count(7, 1) end it 'filters issues by upcoming milestones' do @@ -376,7 +379,7 @@ describe 'Filter issues', feature: true do context 'only text', js: true do it 'filters issues by searched text' do input_filtered_search('Bug') - expect_issues_list_count(4) + expect_issues_list_count(4, 1) end it 'filters issues by multiple searched text' do @@ -468,7 +471,62 @@ describe 'Filter issues', feature: true do end context 'sorting', js: true do - # TODO + it 'sorts by oldest updated' do + create(:issue, + title: '3 days ago', + project: project, + author: user, + created_at: 3.days.ago, + updated_at: 3.days.ago) + + old_issue = create(:issue, + title: '5 days ago', + project: project, + author: user, + created_at: 5.days.ago, + updated_at: 5.days.ago) + + input_filtered_search('days ago') + expect_issues_list_count(2) + + sort_toggle = find('.filtered-search-container .dropdown-toggle') + sort_toggle.click + + find('.filtered-search-container .dropdown-menu li a', text: 'Oldest updated').click + wait_for_ajax + + expect(find('.issues-list .issue:first-of-type .issue-title-text a')).to have_content(old_issue.title) + end + end + end + + describe 'retains filter when switching issue states', js: true do + before do + input_filtered_search('bug') + expect_issues_list_count(4, 1) + end + + it 'open state' do + find('.issues-state-filters a', text: 'Closed').click + wait_for_ajax + + find('.issues-state-filters a', text: 'Open').click + wait_for_ajax + + expect(page).to have_selector('.issues-list .issue', count: 4) + end + + it 'closed state' do + find('.issues-state-filters a', text: 'Closed').click + wait_for_ajax + expect(page).to have_selector('.issues-list .issue', count: 1) + expect(find('.issues-list .issue:first-of-type .issue-title-text a')).to have_content(closed_issue.title) + end + + it 'all state' do + find('.issues-state-filters a', text: 'All').click + wait_for_ajax + expect(page).to have_selector('.issues-list .issue', count: 5) end end diff --git a/spec/features/merge_requests/reset_filters_spec.rb b/spec/features/merge_requests/reset_filters_spec.rb index 2e468f2edf3..3a7ece7e1d6 100644 --- a/spec/features/merge_requests/reset_filters_spec.rb +++ b/spec/features/merge_requests/reset_filters_spec.rb @@ -9,7 +9,7 @@ feature 'Issues filter reset button', feature: true, js: true do let!(:milestone) { create(:milestone, project: project) } let!(:bug) { create(:label, project: project, name: 'bug')} let!(:mr1) { create(:merge_request, title: "Feature", source_project: project, target_project: project, source_branch: "Feature", milestone: milestone, author: user, assignee: user) } - let!(:mr2) { create(:merge_request, title:"Bugfix1", source_project: project, target_project: project, source_branch: "Bugfix1") } + let!(:mr2) { create(:merge_request, title: "Bugfix1", source_project: project, target_project: project, source_branch: "Bugfix1") } let(:merge_request_css) { '.merge-request' } -- cgit v1.2.1 From 976893ec2fa1e4289f5d923a41d296e170bdf3af Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 14 Nov 2016 22:14:29 -0600 Subject: Add support for labels containing single/double quote --- .../filtered_search/filtered_search_tokenizer.es6 | 14 ++++++++++++-- spec/features/issues/filter_issues_spec.rb | 12 +++++------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 index c3e5e817c9e..eab805c4714 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 @@ -62,10 +62,20 @@ const keyMatch = this.validTokenKeys.filter(v => v.key === tokenKey)[0]; const symbolMatch = this.validTokenKeys.filter(v => v.symbol === tokenSymbol)[0]; - if (tokenValue.indexOf('"') !== -1) { + const doubleQuoteIndex = tokenValue.indexOf('"'); + const singleQuoteIndex = tokenValue.indexOf('\''); + + const doubleQuoteExist = doubleQuoteIndex !== -1; + const singleQuoteExist = singleQuoteIndex !== -1; + + if ((doubleQuoteExist && !singleQuoteExist) || + (doubleQuoteExist && singleQuoteExist && doubleQuoteIndex < singleQuoteIndex)) { + // " is found and is in front of ' (if any) lastQuotation = '"'; incompleteToken = true; - } else if (tokenValue.indexOf('\'') !== -1) { + } else if ((singleQuoteExist && !doubleQuoteExist) || + (doubleQuoteExist && singleQuoteExist && singleQuoteIndex < doubleQuoteIndex)) { + // ' is found and is in front of " (if any) lastQuotation = '\''; incompleteToken = true; } diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index 0eed0ed4274..ba9f7579627 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -242,14 +242,12 @@ describe 'Filter issues', feature: true do end it 'single quotes containing double quotes' do - # TODO: Actual bug + double_quotes_label = create(:label, project: project, title: 'won"t fix') + double_quotes_label_issue = create(:issue, title: "Issue with double quotes label", project: project) + double_quotes_label_issue.labels << double_quotes_label - # double_quotes_label = create(:label, project: project, title: 'won"t fix') - # double_quotes_label_issue = create(:issue, title: "Issue with double quotes label", project: project) - # double_quotes_label_issue.labels << double_quotes_label - - # input_filtered_search("label:'#{double_quotes_label.title}'") - # expect_issues_list_count(1) + input_filtered_search("label:~'#{double_quotes_label.title}'") + expect_issues_list_count(1) end it 'double quotes containing single quotes' do -- cgit v1.2.1 From b50b916b8c4f6db2fd5aa3f6536ec493c9f4cb0b Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 15 Nov 2016 12:28:48 -0600 Subject: Fix failing spec --- spec/features/issues/filter_issues_spec.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index ba9f7579627..608e6f20748 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -501,7 +501,9 @@ describe 'Filter issues', feature: true do describe 'retains filter when switching issue states', js: true do before do input_filtered_search('bug') - expect_issues_list_count(4, 1) + + # Wait for search results to load + sleep 1 end it 'open state' do -- cgit v1.2.1 From 3d67042275f61f515cc4c3312944e337563fc658 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 15 Nov 2016 12:55:37 -0600 Subject: Add spec for issue label clicked --- spec/features/issues/filter_issues_spec.rb | 210 ++++++++++++++++++++++------- 1 file changed, 165 insertions(+), 45 deletions(-) diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index 608e6f20748..cbb11b790ec 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -3,9 +3,8 @@ require 'rails_helper' describe 'Filter issues', feature: true do include WaitForAjax - let!(:group) { create(:group) } let!(:project) { create(:project) } - let!(:user) { create(:user)} + let!(:group) { create(:group) } let!(:user) { create(:user) } let!(:user2) { create(:user) } let!(:milestone) { create(:milestone, project: project) } @@ -25,6 +24,10 @@ describe 'Filter issues', feature: true do filtered_search.send_keys(:enter) end + def expect_filtered_search_input(input) + expect(find('.filtered-search').value).to eq(input) + end + def expect_no_issues_list page.within '.issues-list' do expect(page).not_to have_selector('.issue') @@ -112,23 +115,31 @@ describe 'Filter issues', feature: true do context 'author with other filters', js: true do it 'filters issues by searched author and text' do - input_filtered_search("author:@#{user.username} issue") + search = "author:@#{user.username} issue" + input_filtered_search(search) expect_issues_list_count(3) + expect_filtered_search_input(search) end it 'filters issues by searched author, assignee and text' do - input_filtered_search("author:@#{user.username} assignee:@#{user.username} issue") + search = "author:@#{user.username} assignee:@#{user.username} issue" + input_filtered_search(search) expect_issues_list_count(3) + expect_filtered_search_input(search) end it 'filters issues by searched author, assignee, label, and text' do - input_filtered_search("author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} issue") + search = "author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} issue" + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end it 'filters issues by searched author, assignee, label, milestone and text' do - input_filtered_search("author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} issue") + search = "author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} issue" + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end end @@ -140,13 +151,17 @@ describe 'Filter issues', feature: true do describe 'filter issues by assignee' do context 'only assignee', js: true do it 'filters issues by searched assignee' do - input_filtered_search("assignee:@#{user.username}") + search = "assignee:@#{user.username}" + input_filtered_search(search) expect_issues_list_count(5) + expect_filtered_search_input(search) end it 'filters issues by no assignee' do - input_filtered_search("assignee:none") + search = "assignee:none" + input_filtered_search(search) expect_issues_list_count(8, 1) + expect_filtered_search_input(search) end it 'filters issues by invalid assignee' do @@ -160,23 +175,31 @@ describe 'Filter issues', feature: true do context 'assignee with other filters', js: true do it 'filters issues by searched assignee and text' do - input_filtered_search("assignee:@#{user.username} searchTerm") + search = "assignee:@#{user.username} searchTerm" + input_filtered_search(search) expect_issues_list_count(2) + expect_filtered_search_input(search) end it 'filters issues by searched assignee, author and text' do - input_filtered_search("assignee:@#{user.username} author:@#{user.username} searchTerm") + search = "assignee:@#{user.username} author:@#{user.username} searchTerm" + input_filtered_search(search) expect_issues_list_count(2) + expect_filtered_search_input(search) end it 'filters issues by searched assignee, author, label, text' do - input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} searchTerm") + search = "assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} searchTerm" + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end it 'filters issues by searched assignee, author, label, milestone and text' do - input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} searchTerm") + search = "assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} searchTerm" + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end end @@ -188,13 +211,17 @@ describe 'Filter issues', feature: true do describe 'filter issues by label' do context 'only label', js: true do it 'filters issues by searched label' do - input_filtered_search("label:~#{bug_label.title}") + search = "label:~#{bug_label.title}" + input_filtered_search(search) expect_issues_list_count(2) + expect_filtered_search_input(search) end it 'filters issues by no label' do - input_filtered_search("label:none") + search = "label:none" + input_filtered_search(search) expect_issues_list_count(9, 1) + expect_filtered_search_input(search) end it 'filters issues by invalid label' do @@ -202,22 +229,30 @@ describe 'Filter issues', feature: true do end it 'filters issues by multiple labels' do - input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title}") + search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title}" + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end it 'filters issues by label containing special characters' do special_label = create(:label, project: project, title: '!@#{$%^&*()-+[]<>?/:{}|\}') special_issue = create(:issue, title: "Issue with special character label", project: project) special_issue.labels << special_label - input_filtered_search("label:~#{special_label.title}") + + search = "label:~#{special_label.title}" + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end it 'does not show issues' do new_label = create(:label, project: project, title: "new_label") - input_filtered_search("label:~#{new_label.title}") + + search = "label:~#{new_label.title}" + input_filtered_search(search) expect_no_issues_list() + expect_filtered_search_input(search) end end @@ -227,18 +262,27 @@ describe 'Filter issues', feature: true do special_multiple_issue = create(:issue, title: "Issue with special character multiple words label", project: project) special_multiple_issue.labels << special_multiple_label - input_filtered_search("label:~'#{special_multiple_label.title}'") + search = "label:~'#{special_multiple_label.title}'" + input_filtered_search(search) expect_issues_list_count(1) + + # filtered search defaults quotations to double quotes + expect_filtered_search_input("label:~\"#{special_multiple_label.title}\"") end it 'single quotes' do - input_filtered_search("label:~'#{multiple_words_label.title}'") + search = "label:~'#{multiple_words_label.title}'" + input_filtered_search(search) expect_issues_list_count(1) + + expect_filtered_search_input("label:~\"#{multiple_words_label.title}\"") end it 'double quotes' do - input_filtered_search("label:~\"#{multiple_words_label.title}\"") + search = "label:~\"#{multiple_words_label.title}\"" + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end it 'single quotes containing double quotes' do @@ -246,8 +290,10 @@ describe 'Filter issues', feature: true do double_quotes_label_issue = create(:issue, title: "Issue with double quotes label", project: project) double_quotes_label_issue.labels << double_quotes_label - input_filtered_search("label:~'#{double_quotes_label.title}'") + search = "label:~'#{double_quotes_label.title}'" + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end it 'double quotes containing single quotes' do @@ -255,61 +301,88 @@ describe 'Filter issues', feature: true do single_quotes_label_issue = create(:issue, title: "Issue with single quotes label", project: project) single_quotes_label_issue.labels << single_quotes_label - input_filtered_search("label:~\"#{single_quotes_label.title}\"") + search = "label:~\"#{single_quotes_label.title}\"" + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end end context 'label with other filters', js: true do it 'filters issues by searched label and text' do - input_filtered_search("label:~#{caps_sensitive_label.title} bug") + search = "label:~#{caps_sensitive_label.title} bug" + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end it 'filters issues by searched label, author and text' do - input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} bug") + search = "label:~#{caps_sensitive_label.title} author:@#{user.username} bug" + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end it 'filters issues by searched label, author, assignee and text' do - input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug") + search = "label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug" + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end it 'filters issues by searched label, author, assignee, milestone and text' do - input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug") + search = "label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug" + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end end context 'multiple labels with other filters', js: true do it 'filters issues by searched label, label2, and text' do - input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} bug") + search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} bug" + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end it 'filters issues by searched label, label2, author and text' do - input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} bug") + search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} bug" + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end it 'filters issues by searched label, label2, author, assignee and text' do - input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug") + search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug" + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end it 'filters issues by searched label, label2, author, assignee, milestone and text' do - input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug") + search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug" + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end end - it "selects and unselects `won't fix`" do - find('.dropdown-menu-labels a', text: wontfix.title).click - find('.dropdown-menu-labels a', text: wontfix.title).click + context 'issue label clicked', js: true do + before do + find('.issues-list .issue .issue-info a .label', text: multiple_words_label.title).click + sleep 1 + end + + it 'filters' do + expect_issues_list_count(1) + end + + it 'displays in search bar' do + expect(find('.filtered-search').value).to eq("label:~\"#{multiple_words_label.title}\"") + end + end - find('.dropdown-menu-close-icon').click - expect(page).not_to have_css('.filtered-labels') context 'sorting', js: true do # TODO end @@ -343,28 +416,50 @@ describe 'Filter issues', feature: true do it 'filters issues by milestone containing special characters' do special_milestone = create(:milestone, title: '!@\#{$%^&*()}', project: project) create(:issue, title: "Issue with special character milestone", project: project, milestone: special_milestone) - input_filtered_search('milestone:%!@\#{$%^&*()}') + + search = "milestone:%#{special_milestone.title}" + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end it 'does not show issues' do new_milestone = create(:milestone, title: "new", project: project) - input_filtered_search("milestone:%#{new_milestone}") + + search = "milestone:%#{new_milestone.title}" + input_filtered_search(search) expect_no_issues_list() + expect_filtered_search_input(search) end end context 'milestone with other filters', js: true do it 'filters issues by searched milestone and text' do + search = "milestone:%#{milestone.title} bug" + input_filtered_search(search) + expect_issues_list_count(2) + expect_filtered_search_input(search) end it 'filters issues by searched milestone, author and text' do + search = "milestone:%#{milestone.title} author:@#{user.username} bug" + input_filtered_search(search) + expect_issues_list_count(2) + expect_filtered_search_input(search) end it 'filters issues by searched milestone, author, assignee and text' do + search = "milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} bug" + input_filtered_search(search) + expect_issues_list_count(2) + expect_filtered_search_input(search) end it 'filters issues by searched milestone, author, assignee, label and text' do + search = "milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug" + input_filtered_search(search) + expect_issues_list_count(2) + expect_filtered_search_input(search) end end @@ -376,38 +471,52 @@ describe 'Filter issues', feature: true do describe 'filter issues by text' do context 'only text', js: true do it 'filters issues by searched text' do - input_filtered_search('Bug') + search = 'Bug' + input_filtered_search(search) expect_issues_list_count(4, 1) + expect_filtered_search_input(search) end it 'filters issues by multiple searched text' do - input_filtered_search('Bug report') + search = 'Bug report' + input_filtered_search(search) expect_issues_list_count(3) + expect_filtered_search_input(search) end it 'filters issues by case insensitive searched text' do - input_filtered_search('bug report') + search = 'bug report' + input_filtered_search(search) expect_issues_list_count(3) + expect_filtered_search_input(search) end it 'filters issues by searched text containing single quotes' do - input_filtered_search('\'single quotes\'') + search = '\'single quotes\'' + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end it 'filters issues by searched text containing double quotes' do - input_filtered_search('"double quotes"') + search = '"double quotes"' + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end it 'filters issues by searched text containing special characters' do - input_filtered_search('!@#{$%^&*()-+') + search = '!@#{$%^&*()-+' + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end it 'does not show any issues' do - input_filtered_search('testing') + search = 'testing' + input_filtered_search(search) expect_no_issues_list() + expect_filtered_search_input(search) end end @@ -415,56 +524,67 @@ describe 'Filter issues', feature: true do it 'filters issues by searched text and author' do input_filtered_search("bug author:@#{user.username}") expect_issues_list_count(2) + expect_filtered_search_input("author:@#{user.username} bug") end it 'filters issues by searched text, author and more text' do input_filtered_search("bug author:@#{user.username} report") expect_issues_list_count(1) + expect_filtered_search_input("author:@#{user.username} bug report") end it 'filters issues by searched text, author and assignee' do input_filtered_search("bug author:@#{user.username} assignee:@#{user.username}") expect_issues_list_count(2) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug") end it 'filters issues by searched text, author, more text and assignee' do input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username}") expect_issues_list_count(1) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug report") end it 'filters issues by searched text, author, more text, assignee and even more text' do input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with") expect_issues_list_count(1) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug report with") end it 'filters issues by searched text, author, assignee and label' do input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title}") expect_issues_list_count(2) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug") end it 'filters issues by searched text, author, text, assignee, text, label and text' do input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything") expect_issues_list_count(1) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug report with everything") end it 'filters issues by searched text, author, assignee, label and milestone' do input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title}") expect_issues_list_count(2) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title} bug") end it 'filters issues by searched text, author, text, assignee, text, label, text, milestone and text' do input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything milestone:%#{milestone.title} you") expect_issues_list_count(1) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title} bug report with everything you") end it 'filters issues by searched text, author, assignee, multiple labels and milestone' do input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title}") expect_issues_list_count(1) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} bug") end it 'filters issues by searched text, author, text, assignee, text, label1, text, label2, text, milestone and text' do input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything label:~#{caps_sensitive_label.title} you milestone:%#{milestone.title} thought") expect_issues_list_count(1) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} bug report with everything you thought") end end @@ -503,7 +623,7 @@ describe 'Filter issues', feature: true do input_filtered_search('bug') # Wait for search results to load - sleep 1 + sleep 2 end it 'open state' do -- cgit v1.2.1 From 8ecc2117db3a38961785fcaa4b49bd6de13371d4 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 30 Nov 2016 12:30:52 -0600 Subject: Refactor validTokenKeys --- .../filtered_search/filtered_search_manager.js.es6 | 51 +++--------------- .../filtered_search_token_keys.js.es6 | 45 ++++++++++++++++ .../filtered_search/filtered_search_tokenizer.es6 | 60 +++++++++++----------- 3 files changed, 81 insertions(+), 75 deletions(-) create mode 100644 app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 8568bf78416..3899181a352 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -1,42 +1,5 @@ /* eslint-disable no-param-reassign */ ((global) => { - const validTokenKeys = [{ - key: 'author', - type: 'string', - param: 'username', - symbol: '@', - }, { - key: 'assignee', - type: 'string', - param: 'username', - symbol: '@', - conditions: [{ - keyword: 'none', - url: 'assignee_id=0', - }], - }, { - key: 'milestone', - type: 'string', - param: 'title', - symbol: '%', - conditions: [{ - keyword: 'none', - url: 'milestone_title=No+Milestone', - }, { - keyword: 'upcoming', - url: 'milestone_title=%23upcoming', - }], - }, { - key: 'label', - type: 'array', - param: 'name[]', - symbol: '~', - conditions: [{ - keyword: 'none', - url: 'label_name[]=No+Label', - }], - }]; - function clearSearch(e) { e.stopPropagation(); e.preventDefault(); @@ -66,9 +29,9 @@ const key = decodeURIComponent(split[0]); const value = split[1]; - // Check if it matches edge conditions listed in validTokenKeys + // Check if it matches edge conditions listed in gl.FilteredSearchTokenKeys.get() let conditionIndex = 0; - const validCondition = validTokenKeys + const validCondition = gl.FilteredSearchTokenKeys.get() .filter(v => v.conditions && v.conditions.filter((c, index) => { if (c.url === p) { conditionIndex = index; @@ -82,7 +45,7 @@ // Sanitize value since URL converts spaces into + // Replace before decode so that we know what was originally + versus the encoded + const sanitizedValue = value ? decodeURIComponent(value.replace(/[+]/g, ' ')) : value; - const match = validTokenKeys.filter(t => key === `${t.key}_${t.param}`)[0]; + const match = gl.FilteredSearchTokenKeys.get().filter(t => key === `${t.key}_${t.param}`)[0]; if (match) { const sanitizedKey = key.slice(0, key.indexOf('_')); @@ -116,7 +79,7 @@ class FilteredSearchManager { constructor() { - this.tokenizer = new gl.FilteredSearchTokenizer(validTokenKeys); + this.tokenizer = gl.FilteredSearchTokenizer; this.bindEvents(); loadSearchParamsFromURL(); } @@ -131,6 +94,7 @@ document.querySelector('.clear-search').addEventListener('click', clearSearch); } + // TODO: This is only used for testing, remove when going to PRO processInput(e) { const input = e.target.value; this.tokenizer.processTokens(input); @@ -155,8 +119,7 @@ const defaultState = 'opened'; let currentState = defaultState; - const tokens = this.tokenizer.getTokens(); - const searchToken = this.tokenizer.getSearchToken(); + const { tokens, searchToken } = this.tokenizer.processTokens(document.querySelector('.filtered-search').value); if (stateIndex !== -1) { const remaining = currentPath.slice(stateIndex + 6); @@ -167,7 +130,7 @@ path += `&state=${currentState}`; tokens.forEach((token) => { - const match = validTokenKeys.filter(t => t.key === token.key)[0]; + const match = gl.FilteredSearchTokenKeys.get().filter(t => t.key === token.key)[0]; let tokenPath = ''; if (token.wildcard && match.conditions) { diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 new file mode 100644 index 00000000000..8d38a29a354 --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 @@ -0,0 +1,45 @@ +/* eslint-disable no-param-reassign */ +((global) => { + class FilteredSearchTokenKeys { + static get() { + return [{ + key: 'author', + type: 'string', + param: 'username', + symbol: '@', + }, { + key: 'assignee', + type: 'string', + param: 'username', + symbol: '@', + conditions: [{ + keyword: 'none', + url: 'assignee_id=0', + }], + }, { + key: 'milestone', + type: 'string', + param: 'title', + symbol: '%', + conditions: [{ + keyword: 'none', + url: 'milestone_title=No+Milestone', + }, { + keyword: 'upcoming', + url: 'milestone_title=%23upcoming', + }], + }, { + key: 'label', + type: 'array', + param: 'name[]', + symbol: '~', + conditions: [{ + keyword: 'none', + url: 'label_name[]=No+Label', + }], + }]; + } + } + + global.FilteredSearchTokenKeys = FilteredSearchTokenKeys; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 index eab805c4714..b1f37443aa1 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 @@ -1,33 +1,20 @@ /* eslint-disable no-param-reassign */ ((global) => { class FilteredSearchTokenizer { - constructor(validTokenKeys) { - this.validTokenKeys = validTokenKeys; - this.resetTokens(); - } - - getTokens() { - return this.tokens; - } - - getSearchToken() { - return this.searchToken; - } - - resetTokens() { - this.tokens = []; - this.searchToken = ''; - } - - printTokens() { + // TODO: Remove when going to pro + static printTokens(tokens, searchToken, lastToken) { console.log('tokens:'); - this.tokens.forEach(token => console.log(token)); - console.log(`search: ${this.searchToken}`); + tokens.forEach(token => console.log(token)); + console.log(`search: ${searchToken}`); + console.log('last token:'); + console.log(lastToken); } - processTokens(input) { - // Re-calculate tokens - this.resetTokens(); + static processTokens(input) { + let tokens = []; + let searchToken = ''; + let lastToken = ''; + const validTokenKeys = gl.FilteredSearchTokenKeys.get(); const inputs = input.split(' '); let searchTerms = ''; @@ -36,16 +23,17 @@ inputs.forEach((i) => { if (incompleteToken) { - const prevToken = this.tokens.last(); + const prevToken = tokens.last(); prevToken.value += ` ${i}`; // Remove last quotation const lastQuotationRegex = new RegExp(lastQuotation, 'g'); prevToken.value = prevToken.value.replace(lastQuotationRegex, ''); - this.tokens[this.tokens.length - 1] = prevToken; + tokens[tokens.length - 1] = prevToken; // Check to see if this quotation completes the token value if (i.indexOf(lastQuotation)) { + lastToken = tokens.last(); incompleteToken = !incompleteToken; } @@ -59,8 +47,8 @@ const tokenValue = i.slice(colonIndex + 1); const tokenSymbol = tokenValue[0]; console.log(tokenSymbol) - const keyMatch = this.validTokenKeys.filter(v => v.key === tokenKey)[0]; - const symbolMatch = this.validTokenKeys.filter(v => v.symbol === tokenSymbol)[0]; + const keyMatch = validTokenKeys.filter(v => v.key === tokenKey)[0]; + const symbolMatch = validTokenKeys.filter(v => v.symbol === tokenSymbol)[0]; const doubleQuoteIndex = tokenValue.indexOf('"'); const singleQuoteIndex = tokenValue.indexOf('\''); @@ -81,11 +69,12 @@ } if (keyMatch && tokenValue.length > 0) { - this.tokens.push({ + tokens.push({ key: keyMatch.key, value: tokenValue, wildcard: symbolMatch ? false : true, }); + lastToken = tokens.last(); return; } @@ -93,10 +82,19 @@ // Add space for next term searchTerms += `${i} `; + lastToken = i; }, this); - this.searchToken = searchTerms.trim(); - this.printTokens(); + searchToken = searchTerms.trim(); + + // TODO: Remove when going to PRO + gl.FilteredSearchTokenizer.printTokens(tokens, searchToken, lastToken); + + return { + tokens, + searchToken, + lastToken, + }; } } -- cgit v1.2.1 From 3492ff6784ffdd72db2863aa982426b29245ed69 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 30 Nov 2016 12:32:10 -0600 Subject: Add static methods for dropdowns to interface with --- .../filtered_search/filtered_search_manager.js.es6 | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 3899181a352..09a7779635f 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -84,6 +84,21 @@ loadSearchParamsFromURL(); } + static fillInWord(word) { + const originalValue = document.querySelector('.filtered-search').value; + document.querySelector('.filtered-search').value = `${originalValue} ${word.trim()}`; + } + + static loadDropdown(dropdownName) { + dropdownName = dropdownName.toLowerCase(); + + const match = gl.FilteredSearchTokenKeys.get().filter(value => value.key === dropdownName)[0]; + + if (match) { + console.log(`🦄 load ${match.key} dropdown`); + } + } + bindEvents() { const filteredSearchInput = document.querySelector('.filtered-search'); -- cgit v1.2.1 From 44187782bfc7944b535e3feda05557831518806b Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 30 Nov 2016 12:48:54 -0600 Subject: Add type button for accessibility --- app/views/shared/issuable/_search_bar.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 5e759301a04..4c27c835bee 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -14,7 +14,7 @@ .filtered-search-input-container %input.form-control.filtered-search{ placeholder: 'Search or filter results...' } = icon('filter') - %button.clear-search.hidden + %button.clear-search.hidden{ type: 'button' } = icon('times') .pull-right - if boards_page -- cgit v1.2.1 From 64d46a3e80001c2dc13f6fd04e2abac40ee9d093 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 30 Nov 2016 15:18:17 -0600 Subject: Add logic for dynamically selecting which dropdown to load [skip ci] --- .../filtered_search/filtered_search_manager.js.es6 | 55 ++++++++++++++++++---- .../filtered_search/filtered_search_tokenizer.es6 | 35 ++++++++++---- app/views/shared/issuable/_search_bar.html.haml | 2 +- 3 files changed, 72 insertions(+), 20 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 09a7779635f..8903f382c18 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -77,42 +77,77 @@ } } + let dropdownHint; + class FilteredSearchManager { constructor() { this.tokenizer = gl.FilteredSearchTokenizer; this.bindEvents(); loadSearchParamsFromURL(); + this.setDropdown(); } - static fillInWord(word) { - const originalValue = document.querySelector('.filtered-search').value; - document.querySelector('.filtered-search').value = `${originalValue} ${word.trim()}`; + static addWordToInput(word, addSpace) { + const hasExistingValue = document.querySelector('.filtered-search').value.length !== 0; + document.querySelector('.filtered-search').value += hasExistingValue && addSpace ? ` ${word}` : word; } - static loadDropdown(dropdownName) { + loadDropdown(dropdownName = '') { dropdownName = dropdownName.toLowerCase(); const match = gl.FilteredSearchTokenKeys.get().filter(value => value.key === dropdownName)[0]; - if (match) { + if (match && this.currentDropdown !== match.key) { console.log(`🦄 load ${match.key} dropdown`); + this.currentDropdown = match.key; + } else if (!match && this.currentDropdown !== 'hint') { + console.log('🦄 load hint dropdown'); + this.currentDropdown = 'hint'; + + if (!dropdownHint) { + dropdownHint = new gl.DropdownHint(document.querySelector('#js-dropdown-hint'), document.querySelector('.filtered-search')) + } + + dropdownHint.render(); + } + } + + setDropdown() { + const { lastToken } = this.tokenizer.processTokens(document.querySelector('.filtered-search').value); + + if (typeof lastToken === 'string') { + // Token is not fully initialized yet + // because it has no value + // Eg. token = 'label:' + const { tokenKey } = this.tokenizer.parseToken(lastToken); + this.loadDropdown(tokenKey); + } else if (lastToken.hasOwnProperty('key')) { + // Token has been initialized into an object + // because it has a value + this.loadDropdown(lastToken.key); + } else { + this.loadDropdown('hint'); } } bindEvents() { const filteredSearchInput = document.querySelector('.filtered-search'); - filteredSearchInput.addEventListener('input', this.processInput.bind(this)); + filteredSearchInput.addEventListener('input', this.setDropdown.bind(this)); filteredSearchInput.addEventListener('input', toggleClearSearchButton); filteredSearchInput.addEventListener('keydown', this.checkForEnter.bind(this)); - document.querySelector('.clear-search').addEventListener('click', clearSearch); } - // TODO: This is only used for testing, remove when going to PRO - processInput(e) { + checkDropdownToken(e) { const input = e.target.value; - this.tokenizer.processTokens(input); + const { lastToken } = this.tokenizer.processTokens(input); + + // Check for dropdown token + if (lastToken[lastToken.length - 1] === ':') { + const token = lastToken.slice(0, -1); + + } } checkForEnter(e) { diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 index b1f37443aa1..b686a43cf32 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 @@ -3,11 +3,30 @@ class FilteredSearchTokenizer { // TODO: Remove when going to pro static printTokens(tokens, searchToken, lastToken) { - console.log('tokens:'); - tokens.forEach(token => console.log(token)); - console.log(`search: ${searchToken}`); - console.log('last token:'); - console.log(lastToken); + // console.log('tokens:'); + // tokens.forEach(token => console.log(token)); + // console.log(`search: ${searchToken}`); + // console.log('last token:'); + // console.log(lastToken); + } + + static parseToken(input) { + const colonIndex = input.indexOf(':'); + let tokenKey; + let tokenValue; + let tokenSymbol; + + if (colonIndex !== -1) { + tokenKey = input.slice(0, colonIndex).toLowerCase(); + tokenValue = input.slice(colonIndex + 1); + tokenSymbol = tokenValue[0]; + } + + return { + tokenKey, + tokenValue, + tokenSymbol, + } } static processTokens(input) { @@ -43,10 +62,8 @@ const colonIndex = i.indexOf(':'); if (colonIndex !== -1) { - const tokenKey = i.slice(0, colonIndex).toLowerCase(); - const tokenValue = i.slice(colonIndex + 1); - const tokenSymbol = tokenValue[0]; - console.log(tokenSymbol) + const { tokenKey, tokenValue, tokenSymbol } = gl.FilteredSearchTokenizer.parseToken(i); + const keyMatch = validTokenKeys.filter(v => v.key === tokenKey)[0]; const symbolMatch = validTokenKeys.filter(v => v.symbol === tokenSymbol)[0]; diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 4c27c835bee..a45af053f5c 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -12,7 +12,7 @@ class: "check_all_issues left" .issues-other-filters.filtered-search-container .filtered-search-input-container - %input.form-control.filtered-search{ placeholder: 'Search or filter results...' } + %input.form-control.filtered-search{ placeholder: 'Search or filter results...', 'data-id' => 'filtered-search' } = icon('filter') %button.clear-search.hidden{ type: 'button' } = icon('times') -- cgit v1.2.1 From 3c0755809f82b4eed5913f1994f57ccffffb4686 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 30 Nov 2016 15:25:10 -0600 Subject: Add dropdown hint --- .../filtered_search/dropdown_hint.js.es6 | 106 +++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 app/assets/javascripts/filtered_search/dropdown_hint.js.es6 diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 new file mode 100644 index 00000000000..ebbd43ad8e0 --- /dev/null +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -0,0 +1,106 @@ +/* eslint-disable no-param-reassign */ +((global) => { + const dropdownData = [{ + icon: 'fa-search', + hint: 'Keep typing and press Enter', + tag: '', + },{ + icon: 'fa-pencil', + hint: 'author:', + tag: '<author>' + },{ + icon: 'fa-user', + hint: 'assignee:', + tag: '<assignee>', + },{ + icon: 'fa-clock-o', + hint: 'milestone:', + tag: '<milestone>', + },{ + icon: 'fa-tag', + hint: 'label:', + tag: '<label>', + }]; + + class DropdownHint { + constructor(dropdown, input) { + this.input = input; + this.dropdown = dropdown; + this.bindEvents(); + } + + bindEvents() { + this.dropdown.addEventListener('click.dl', this.itemClicked.bind(this)); + } + + unbindEvents() { + this.dropdown.removeEventListener('click.dl', this.itemClicked.bind(this)); + } + + // cleanup() { + // this.unbindEvents(); + // droplab.setConfig({'filtered-search': {}}); + // droplab.setData('filtered-search', []); + // this.dropdown.style.display = 'hidden'; + // } + + getSelectedText(selectedToken) { + // TODO: Get last word from FilteredSearchTokenizer + const lastWord = this.input.value.split(' ').last(); + const lastWordIndex = selectedToken.indexOf(lastWord); + + return lastWordIndex === -1 ? selectedToken : selectedToken.slice(lastWord.length); + } + + itemClicked(e) { + const token = e.detail.selected.querySelector('.js-filter-hint').innerText.trim(); + const tag = e.detail.selected.querySelector('.js-filter-tag').innerText.trim(); + + if (tag.length) { + gl.FilteredSearchManager.addWordToInput(this.getSelectedText(token)); + } + + this.input.focus(); + this.dismissDropdown(); + + // Propogate input change to FilteredSearchManager + // so that it can determine which dropdowns to open + this.input.dispatchEvent(new Event('input')); + } + + dismissDropdown() { + this.input.removeAttribute('data-dropdown-trigger'); + droplab.setConfig({'filtered-search': {}}); + droplab.setData('filtered-search', []); + this.unbindEvents(); + } + + setAsDropdown() { + this.input.setAttribute('data-dropdown-trigger', '#js-dropdown-hint'); + // const hookId = 'filtered-search'; + // const listId = 'js-dropdown-hint'; + // const hook = droplab.hooks.filter((h) => { + // return h.id === hookId; + // })[0]; + + // if (hook.list.list.id !== listId) { + // droplab.changeHookList(hookId, `#${listId}`); + // } + } + + render() { + console.log('render dropdown hint'); + this.setAsDropdown(); + + droplab.setConfig({ + 'filtered-search': { + text: 'hint' + } + }); + + droplab.setData('filtered-search', dropdownData); + } + } + + global.DropdownHint = DropdownHint; +})(window.gl || (window.gl = {})); -- cgit v1.2.1 From a1ca5c76ab44e306fb4fb4adcfe5ea2214bd5abc Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 2 Dec 2016 15:02:54 -0600 Subject: Add droplab updates --- app/assets/javascripts/application.js | 1 + app/assets/javascripts/droplab/droplab.js | 98 +++++++++++++++++++----- app/assets/javascripts/droplab/droplab_filter.js | 12 ++- 3 files changed, 86 insertions(+), 25 deletions(-) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index e43afbb4cc9..f0615481ed2 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -58,6 +58,7 @@ /*= require_directory ./extensions */ /*= require_directory ./lib/utils */ /*= require_directory ./u2f */ +/*= require_directory ./droplab */ /*= require_directory . */ /*= require fuzzaldrin-plus */ /*= require es6-promise.auto */ diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js index 18ca8be7203..56582e71b61 100644 --- a/app/assets/javascripts/droplab/droplab.js +++ b/app/assets/javascripts/droplab/droplab.js @@ -38,6 +38,7 @@ var DropDown = function(list, trigger) { this.items = []; this.getItems(); this.addEvents(); + this.initialState = list.innerHTML; }; Object.assign(DropDown.prototype, { @@ -50,7 +51,8 @@ Object.assign(DropDown.prototype, { var self = this; // event delegation. this.list.addEventListener('click', function(e) { - if(e.target.tagName === 'A') { + if(e.target.tagName === 'A' || e.target.tagName === 'button') { + e.preventDefault(); self.hide(); var listEvent = new CustomEvent('click.dl', { detail: { @@ -72,6 +74,11 @@ Object.assign(DropDown.prototype, { } }, + setData: function(data) { + this.data = data; + this.render(data); + }, + addData: function(data) { this.data = (this.data || []).concat(data); this.render(data); @@ -155,8 +162,17 @@ require('./window')(function(w){ addData: function () { var args = [].slice.apply(arguments); + this.applyArgs(args, '_addData'); + }, + + setData: function() { + var args = [].slice.apply(arguments); + this.applyArgs(args, '_setData'); + }, + + applyArgs: function(args, methodName) { if(this.ready) { - this._addData.apply(this, args); + this[methodName].apply(this, args); } else { this.queuedData = this.queuedData || []; this.queuedData.push(args); @@ -164,10 +180,18 @@ require('./window')(function(w){ }, _addData: function(trigger, data) { + this._processData(trigger, data, 'addData'); + }, + + _setData: function(trigger, data) { + this._processData(trigger, data, 'setData'); + }, + + _processData: function(trigger, data, methodName) { this.hooks.forEach(function(hook) { if(hook.trigger.dataset.hasOwnProperty('id')) { if(hook.trigger.dataset.id === trigger) { - hook.list.addData(data); + hook.list[methodName](data); } } }); @@ -189,21 +213,48 @@ require('./window')(function(w){ }); }, - addHook: function(hook) { + changeHookList: function(trigger, list) { + trigger = document.querySelector('[data-id="'+trigger+'"]'); + list = document.querySelector(list); + this.hooks.every(function(hook, i) { + if(hook.trigger === trigger) { + // Restore initial State + hook.list.list.innerHTML = hook.list.initialState; + hook.list.hide(); + hook.trigger.removeEventListener('mousedown', hook.events.mousedown); + hook.trigger.removeEventListener('input', hook.events.input); + hook.trigger.removeEventListener('keyup', hook.events.keyup); + hook.trigger.removeEventListener('keydown', hook.events.keydown); + this.hooks.splice(i, 1); + this.addHook(trigger, list); + return false; + } + return true + }.bind(this)); + }, + + addHook: function(hook, list) { if(!(hook instanceof HTMLElement) && typeof hook === 'string'){ hook = document.querySelector(hook); } - var list = document.querySelector(hook.dataset[utils.toDataCamelCase(DATA_TRIGGER)]); - if(hook.tagName === 'A' || hook.tagName === 'BUTTON') { - this.hooks.push(new HookButton(hook, list)); - } else if(hook.tagName === 'INPUT') { - this.hooks.push(new HookInput(hook, list)); + if(!list){ + list = document.querySelector(hook.dataset[utils.toDataCamelCase(DATA_TRIGGER)]); + } + + if(hook) { + if(hook.tagName === 'A' || hook.tagName === 'BUTTON') { + this.hooks.push(new HookButton(hook, list)); + } else if(hook.tagName === 'INPUT') { + this.hooks.push(new HookInput(hook, list)); + } } return this; }, addHooks: function(hooks) { - hooks.forEach(this.addHook.bind(this)); + hooks.forEach(function(hook) { + this.addHook(hook, null); + }.bind(this)); return this; }, @@ -302,7 +353,8 @@ var HookInput = function(trigger, list) { Object.assign(HookInput.prototype, { addEvents: function(){ var self = this; - this.trigger.addEventListener('mousedown', function(e){ + + function mousedown(e) { var mouseEvent = new CustomEvent('mousedown.dl', { detail: { hook: self, @@ -312,9 +364,9 @@ Object.assign(HookInput.prototype, { cancelable: true }); e.target.dispatchEvent(mouseEvent); - }); + } - this.trigger.addEventListener('input', function(e){ + function input(e) { var inputEvent = new CustomEvent('input.dl', { detail: { hook: self, @@ -325,15 +377,15 @@ Object.assign(HookInput.prototype, { }); e.target.dispatchEvent(inputEvent); self.list.show(); - }); + } - this.trigger.addEventListener('keyup', function(e){ + function keyup(e) { keyEvent(e, 'keyup.dl'); - }); + } - this.trigger.addEventListener('keydown', function(e){ + function keydown(e) { keyEvent(e, 'keydown.dl'); - }); + } function keyEvent(e, keyEventName){ var keyEvent = new CustomEvent(keyEventName, { @@ -349,6 +401,16 @@ Object.assign(HookInput.prototype, { e.target.dispatchEvent(keyEvent); self.list.show(); } + + this.events = this.events || {}; + this.events.mousedown = mousedown; + this.events.input = input; + this.events.keyup = keyup; + this.events.keydown = keydown; + this.trigger.addEventListener('mousedown', mousedown); + this.trigger.addEventListener('input', input); + this.trigger.addEventListener('keyup', keyup); + this.trigger.addEventListener('keydown', keydown); }, }); diff --git a/app/assets/javascripts/droplab/droplab_filter.js b/app/assets/javascripts/droplab/droplab_filter.js index 4a7ae0cbdc1..88e69c02422 100644 --- a/app/assets/javascripts/droplab/droplab_filter.js +++ b/app/assets/javascripts/droplab/droplab_filter.js @@ -2,18 +2,17 @@ (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.filter||(g.filter = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o Date: Fri, 2 Dec 2016 15:04:10 -0600 Subject: Add dropdowns for assignee --- .../filtered_search/dropdown_assignee.js.es6 | 21 ++++++ .../filtered_search/dropdown_hint.js.es6 | 69 +++---------------- .../filtered_search_dropdown.js.es6 | 78 ++++++++++++++++++++++ .../filtered_search/filtered_search_manager.js.es6 | 22 +++++- app/views/shared/issuable/_search_bar.html.haml | 17 +++++ 5 files changed, 147 insertions(+), 60 deletions(-) create mode 100644 app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 create mode 100644 app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 diff --git a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 new file mode 100644 index 00000000000..9e4d1018ac3 --- /dev/null +++ b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 @@ -0,0 +1,21 @@ +/* eslint-disable no-param-reassign */ +/*= require filtered_search/filtered_search_dropdown */ + +((global) => { + class DropdownAssignee extends gl.FilteredSearchDropdown { + constructor(dropdown, input) { + super(dropdown, input); + this.listId = 'js-dropdown-assignee'; + } + + itemClicked(e) { + console.log('assignee clicked'); + } + + renderContent() { + droplab.addData(this.hookId, '/autocomplete/users.json?search=&per_page=20&active=true&project_id=2&group_id=&skip_ldap=&todo_filter=&todo_state_filter=¤t_user=true&push_code_to_protected_branches=&author_id=&skip_users='); + } + } + + global.DropdownAssignee = DropdownAssignee; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index ebbd43ad8e0..0593561c8a1 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -1,4 +1,6 @@ /* eslint-disable no-param-reassign */ +/*= require filtered_search/filtered_search_dropdown */ + ((global) => { const dropdownData = [{ icon: 'fa-search', @@ -22,34 +24,11 @@ tag: '<label>', }]; - class DropdownHint { - constructor(dropdown, input) { - this.input = input; - this.dropdown = dropdown; - this.bindEvents(); - } - - bindEvents() { - this.dropdown.addEventListener('click.dl', this.itemClicked.bind(this)); - } - - unbindEvents() { - this.dropdown.removeEventListener('click.dl', this.itemClicked.bind(this)); - } - - // cleanup() { - // this.unbindEvents(); - // droplab.setConfig({'filtered-search': {}}); - // droplab.setData('filtered-search', []); - // this.dropdown.style.display = 'hidden'; - // } - - getSelectedText(selectedToken) { - // TODO: Get last word from FilteredSearchTokenizer - const lastWord = this.input.value.split(' ').last(); - const lastWordIndex = selectedToken.indexOf(lastWord); - - return lastWordIndex === -1 ? selectedToken : selectedToken.slice(lastWord.length); + class DropdownHint extends gl.FilteredSearchDropdown { + constructor(dropdown, input, filterKeyword) { + super(dropdown, input); + this.listId = 'js-dropdown-hint'; + this.filterKeyword = filterKeyword; } itemClicked(e) { @@ -68,37 +47,9 @@ this.input.dispatchEvent(new Event('input')); } - dismissDropdown() { - this.input.removeAttribute('data-dropdown-trigger'); - droplab.setConfig({'filtered-search': {}}); - droplab.setData('filtered-search', []); - this.unbindEvents(); - } - - setAsDropdown() { - this.input.setAttribute('data-dropdown-trigger', '#js-dropdown-hint'); - // const hookId = 'filtered-search'; - // const listId = 'js-dropdown-hint'; - // const hook = droplab.hooks.filter((h) => { - // return h.id === hookId; - // })[0]; - - // if (hook.list.list.id !== listId) { - // droplab.changeHookList(hookId, `#${listId}`); - // } - } - - render() { - console.log('render dropdown hint'); - this.setAsDropdown(); - - droplab.setConfig({ - 'filtered-search': { - text: 'hint' - } - }); - - droplab.setData('filtered-search', dropdownData); + renderContent() { + super.renderContent(); + droplab.setData(this.hookId, dropdownData); } } diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 new file mode 100644 index 00000000000..250d8236ea9 --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -0,0 +1,78 @@ +/* eslint-disable no-param-reassign */ +((global) => { + const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger'; + + class FilteredSearchDropdown { + constructor(dropdown, input) { + this.hookId = 'filtered-search'; + this.input = input; + this.dropdown = dropdown; + this.bindEvents(); + } + + bindEvents() { + this.dropdown.addEventListener('click.dl', this.itemClicked.bind(this)); + } + + unbindEvents() { + this.dropdown.removeEventListener('click.dl', this.itemClicked.bind(this)); + } + + getSelectedText(selectedToken) { + // TODO: Get last word from FilteredSearchTokenizer + const lastWord = this.input.value.split(' ').last(); + const lastWordIndex = selectedToken.indexOf(lastWord); + + return lastWordIndex === -1 ? selectedToken : selectedToken.slice(lastWord.length); + } + + itemClicked(e) { + // Overridden by dropdown sub class + } + + getFilterConfig(filterKeyword) { + const config = {}; + const filterConfig = { + text: filterKeyword, + }; + + config[this.hookId] = filterKeyword ? filterConfig : {}; + + return config; + } + + dismissDropdown() { + this.input.removeAttribute(DATA_DROPDOWN_TRIGGER); + droplab.setConfig(this.getFilterConfig()); + droplab.setData(this.hookId, []); + this.unbindEvents(); + } + + setAsDropdown() { + this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.listId}`); + } + + getCurrentHook() { + return droplab.hooks.filter(h => h.id === this.hookId)[0]; + } + + renderContent() { + droplab.setConfig(this.getFilterConfig(this.filterKeyword)); + } + + render() { + this.setAsDropdown(); + + const firstTimeInitialized = this.getCurrentHook() === undefined; + + if (firstTimeInitialized) { + this.renderContent(); + } else if(this.getCurrentHook().list.list.id !== this.listId) { + droplab.changeHookList(this.hookId, `#${this.listId}`); + this.renderContent(); + } + } + } + + global.FilteredSearchDropdown = FilteredSearchDropdown; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 8903f382c18..92f07024354 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -78,6 +78,7 @@ } let dropdownHint; + let dropdownAssignee; class FilteredSearchManager { constructor() { @@ -99,19 +100,38 @@ if (match && this.currentDropdown !== match.key) { console.log(`🦄 load ${match.key} dropdown`); + this.dismissCurrentDropdown(); this.currentDropdown = match.key; + + if (match.key === 'assignee') { + if (!dropdownAssignee) { + + // document.querySelector('.filtered-search').setAttribute('data-dropdown-trigger', '#js-dropdown-assignee'); + dropdownAssignee = new gl.DropdownAssignee(document.querySelector('#js-dropdown-assignee'), document.querySelector('.filtered-search')); + } + + dropdownAssignee.render(); + } + } else if (!match && this.currentDropdown !== 'hint') { console.log('🦄 load hint dropdown'); + this.dismissCurrentDropdown(); this.currentDropdown = 'hint'; if (!dropdownHint) { - dropdownHint = new gl.DropdownHint(document.querySelector('#js-dropdown-hint'), document.querySelector('.filtered-search')) + dropdownHint = new gl.DropdownHint(document.querySelector('#js-dropdown-hint'), document.querySelector('.filtered-search'), 'hint'); } dropdownHint.render(); } } + dismissCurrentDropdown() { + if (this.currentDropdown === 'hint') { + dropdownHint.dismissDropdown(); + } + } + setDropdown() { const { lastToken } = this.tokenizer.processTokens(document.querySelector('.filtered-search').value); diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index a45af053f5c..04000a18dce 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -16,6 +16,23 @@ = icon('filter') %button.clear-search.hidden{ type: 'button' } = icon('times') + %ul#js-dropdown-hint.dropdown-menu{ 'data-dynamic' => true } + %li + %button.btn.btn-link + %i.fa{ 'class': '{{icon}}'} + %span.js-filter-hint + {{hint}} + %span.js-filter-tag + {{tag}} + #js-dropdown-assignee.dropdown-menu{ 'data-dropdown' => true } + %ul{ 'data-dynamic' => true } + %li + %button.btn.btn-link + %img.avatar.avatar-inline{ 'src': '{{avatar_url}}', width: '30' } + %strong + {{name}} + %span + {{username}} .pull-right - if boards_page #js-boards-seach.issue-boards-search -- cgit v1.2.1 From 9081d3efeec5b22fd92c76172ae92dad3cc94c58 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 2 Dec 2016 16:20:01 -0600 Subject: Update droplab --- app/assets/javascripts/droplab/droplab.js | 19 +++++++++++++++++-- app/assets/javascripts/droplab/droplab_ajax.js | 11 +++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js index 56582e71b61..aff47aa23cf 100644 --- a/app/assets/javascripts/droplab/droplab.js +++ b/app/assets/javascripts/droplab/droplab.js @@ -51,13 +51,15 @@ Object.assign(DropDown.prototype, { var self = this; // event delegation. this.list.addEventListener('click', function(e) { - if(e.target.tagName === 'A' || e.target.tagName === 'button') { + // climb up the tree to find the LI + var selected = utils.closest(e.target, 'LI'); + if(selected) { e.preventDefault(); self.hide(); var listEvent = new CustomEvent('click.dl', { detail: { list: self, - selected: e.target, + selected: selected, data: e.target.dataset, }, }); @@ -102,6 +104,15 @@ Object.assign(DropDown.prototype, { var html = utils.t(sampleItem.outerHTML, dat); var template = document.createElement('template'); template.innerHTML = html; + + // Help set the image src template + var imageTags = template.content.querySelectorAll('img[data-src]'); + for(var i = 0; i < imageTags.length; i++) { + var imageTag = imagetags[i]; + imageTag.src = imageTag.getAttribute('data-src'); + imageTag.removeAttribute('data-src'); + } + if(dat.hasOwnProperty('droplab_hidden') && dat.droplab_hidden){ template.content.firstChild.style.display = 'none' }else{ @@ -115,6 +126,9 @@ Object.assign(DropDown.prototype, { } else { this.list.innerHTML = newChildren.join(''); } + + // Show dropdown if there is data + data !== [] ? this.show() : this.hide(); }, show: function() { @@ -221,6 +235,7 @@ require('./window')(function(w){ // Restore initial State hook.list.list.innerHTML = hook.list.initialState; hook.list.hide(); + hook.trigger.removeEventListener('mousedown', hook.events.mousedown); hook.trigger.removeEventListener('input', hook.events.input); hook.trigger.removeEventListener('keyup', hook.events.keyup); diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js index 23e43b352d6..2dff5b83fae 100644 --- a/app/assets/javascripts/droplab/droplab_ajax.js +++ b/app/assets/javascripts/droplab/droplab_ajax.js @@ -3,6 +3,7 @@ /* global droplab */ droplab.plugin(function init(DropLab) { var _addData = DropLab.prototype.addData; + var _setData = DropLab.prototype.setData; var _loadUrlData = function(url) { return new Promise(function(resolve, reject) { @@ -24,10 +25,16 @@ droplab.plugin(function init(DropLab) { Object.assign(DropLab.prototype, { addData: function(trigger, data) { + this.processData(trigger, data, _addData); + }, + setData: function(trigger, data) { + this.processData(trigger, data, _setData); + }, + processData: function(trigger, data, methodName) { var _this = this; if('string' === typeof data) { _loadUrlData(data).then(function(d) { - _addData.call(_this, trigger, d); + methodName.call(_this, trigger, d); }).catch(function(e) { if(e.message) console.error(e.message, e.stack); // eslint-disable-line no-console @@ -35,7 +42,7 @@ droplab.plugin(function init(DropLab) { console.error(e); // eslint-disable-line no-console }) } else { - _addData.apply(this, arguments); + methodName.apply(this, arguments); } }, }); -- cgit v1.2.1 From ce1247727d2a9f24994f602debe95fd2ebff90db Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 2 Dec 2016 16:20:21 -0600 Subject: Fix rendering of assignee dropdown after clicking hint dropdown --- app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 | 3 ++- app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 index 9e4d1018ac3..e3cbb4cb3a0 100644 --- a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 @@ -13,7 +13,8 @@ } renderContent() { - droplab.addData(this.hookId, '/autocomplete/users.json?search=&per_page=20&active=true&project_id=2&group_id=&skip_ldap=&todo_filter=&todo_state_filter=¤t_user=true&push_code_to_protected_branches=&author_id=&skip_users='); + super.renderContent(); + droplab.setData(this.hookId, '/autocomplete/users.json?search=&per_page=20&active=true&project_id=2&group_id=&skip_ldap=&todo_filter=&todo_state_filter=¤t_user=true&push_code_to_protected_branches=&author_id=&skip_users='); } } diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 92f07024354..fc7bfe121fb 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -121,7 +121,6 @@ if (!dropdownHint) { dropdownHint = new gl.DropdownHint(document.querySelector('#js-dropdown-hint'), document.querySelector('.filtered-search'), 'hint'); } - dropdownHint.render(); } } -- cgit v1.2.1 From 78b9e7c6b500b87dd2ebac45331a012eb0cf6ca4 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 2 Dec 2016 16:30:19 -0600 Subject: Add author dropdown --- .../filtered_search/dropdown_author.js.es6 | 22 ++++++++++++++++++++++ .../filtered_search/filtered_search_manager.js.es6 | 13 +++++++++---- app/views/shared/issuable/_search_bar.html.haml | 9 +++++++++ 3 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 app/assets/javascripts/filtered_search/dropdown_author.js.es6 diff --git a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 new file mode 100644 index 00000000000..e16b313b743 --- /dev/null +++ b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 @@ -0,0 +1,22 @@ +/* eslint-disable no-param-reassign */ +/*= require filtered_search/filtered_search_dropdown */ + +((global) => { + class DropdownAuthor extends gl.FilteredSearchDropdown { + constructor(dropdown, input) { + super(dropdown, input); + this.listId = 'js-dropdown-author'; + } + + itemClicked(e) { + console.log('author clicked'); + } + + renderContent() { + super.renderContent(); + droplab.setData(this.hookId, '/autocomplete/users.json?search=&per_page=20&active=true&project_id=2&group_id=&skip_ldap=&todo_filter=&todo_state_filter=¤t_user=true&push_code_to_protected_branches=&author_id=&skip_users='); + } + } + + global.DropdownAuthor = DropdownAuthor; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index fc7bfe121fb..237f4fc3fff 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -78,6 +78,7 @@ } let dropdownHint; + let dropdownAuthor; let dropdownAssignee; class FilteredSearchManager { @@ -103,10 +104,14 @@ this.dismissCurrentDropdown(); this.currentDropdown = match.key; - if (match.key === 'assignee') { - if (!dropdownAssignee) { + if (match.key === 'author') { + if (!dropdownAuthor) { + dropdownAuthor = new gl.DropdownAuthor(document.querySelector('#js-dropdown-author'), document.querySelector('.filtered-search')); + } - // document.querySelector('.filtered-search').setAttribute('data-dropdown-trigger', '#js-dropdown-assignee'); + dropdownAuthor.render(); + } else if (match.key === 'assignee') { + if (!dropdownAssignee) { dropdownAssignee = new gl.DropdownAssignee(document.querySelector('#js-dropdown-assignee'), document.querySelector('.filtered-search')); } @@ -119,7 +124,7 @@ this.currentDropdown = 'hint'; if (!dropdownHint) { - dropdownHint = new gl.DropdownHint(document.querySelector('#js-dropdown-hint'), document.querySelector('.filtered-search'), 'hint'); + dropdownHint = new gl.DropdownHint(document.querySelector('#js-dropdown-hint'), document.querySelector('.filtered-search'), this.currentDropdown); } dropdownHint.render(); } diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 04000a18dce..3801b46a332 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -24,6 +24,15 @@ {{hint}} %span.js-filter-tag {{tag}} + #js-dropdown-author.dropdown-menu{ 'data-dropdown' => true } + %ul{ 'data-dynamic' => true } + %li + %button.btn.btn-link + %img.avatar.avatar-inline{ 'src': '{{avatar_url}}', width: '30' } + %strong + {{name}} + %span + {{username}} #js-dropdown-assignee.dropdown-menu{ 'data-dropdown' => true } %ul{ 'data-dynamic' => true } %li -- cgit v1.2.1 From a510791bb2fb537fd2fbe4a9f6b94e38fe5a6094 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 2 Dec 2016 16:43:15 -0600 Subject: Add label and milestone dropdowns --- .../filtered_search/dropdown_label.js.es6 | 22 ++++++++++++++++++++++ .../filtered_search/dropdown_milestone.js.es6 | 22 ++++++++++++++++++++++ .../filtered_search/filtered_search_manager.js.es6 | 14 ++++++++++++++ app/views/shared/issuable/_search_bar.html.haml | 11 +++++++++++ 4 files changed, 69 insertions(+) create mode 100644 app/assets/javascripts/filtered_search/dropdown_label.js.es6 create mode 100644 app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 diff --git a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 new file mode 100644 index 00000000000..9225dca13b0 --- /dev/null +++ b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 @@ -0,0 +1,22 @@ +/* eslint-disable no-param-reassign */ +/*= require filtered_search/filtered_search_dropdown */ + +((global) => { + class DropdownLabel extends gl.FilteredSearchDropdown { + constructor(dropdown, input) { + super(dropdown, input); + this.listId = 'js-dropdown-label'; + } + + itemClicked(e) { + console.log('label clicked'); + } + + renderContent() { + super.renderContent(); + droplab.setData(this.hookId, 'labels.json'); + } + } + + global.DropdownLabel = DropdownLabel; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 new file mode 100644 index 00000000000..ab97d709886 --- /dev/null +++ b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 @@ -0,0 +1,22 @@ +/* eslint-disable no-param-reassign */ +/*= require filtered_search/filtered_search_dropdown */ + +((global) => { + class DropdownMilestone extends gl.FilteredSearchDropdown { + constructor(dropdown, input) { + super(dropdown, input); + this.listId = 'js-dropdown-milestone'; + } + + itemClicked(e) { + console.log('milestone clicked'); + } + + renderContent() { + super.renderContent(); + droplab.setData(this.hookId, 'milestones.json'); + } + } + + global.DropdownMilestone = DropdownMilestone; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 237f4fc3fff..f06d5a646cf 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -80,6 +80,8 @@ let dropdownHint; let dropdownAuthor; let dropdownAssignee; + let dropdownMilestone; + let dropdownLabel; class FilteredSearchManager { constructor() { @@ -116,6 +118,18 @@ } dropdownAssignee.render(); + } else if (match.key === 'milestone') { + if (!dropdownMilestone) { + dropdownMilestone = new gl.DropdownMilestone(document.querySelector('#js-dropdown-milestone'), document.querySelector('.filtered-search')); + } + + dropdownMilestone.render(); + } else if (match.key === 'label') { + if (!dropdownLabel) { + dropdownLabel = new gl.DropdownLabel(document.querySelector('#js-dropdown-label'), document.querySelector('.filtered-search')); + } + + dropdownLabel.render(); } } else if (!match && this.currentDropdown !== 'hint') { diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 3801b46a332..cf5b1a52332 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -42,6 +42,17 @@ {{name}} %span {{username}} + #js-dropdown-milestone.dropdown-menu{ 'data-dropdown' => true } + %ul{ 'data-dynamic' => true } + %li + %button.btn.btn-link + {{title}} + #js-dropdown-label.dropdown-menu{ 'data-dropdown' => true } + %ul{ 'data-dynamic' => true } + %li + %button.btn.btn-link + %span.dropdown-label-box{ 'style': 'background: {{color}}'} + {{title}} .pull-right - if boards_page #js-boards-seach.issue-boards-search -- cgit v1.2.1 From 2edaabfe669f7e865a56eab321db41fe9cdcad89 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 12:18:02 -0600 Subject: Fix image data-src --- app/assets/javascripts/droplab/droplab.js | 2 +- app/views/shared/issuable/_search_bar.html.haml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js index aff47aa23cf..0152eef793f 100644 --- a/app/assets/javascripts/droplab/droplab.js +++ b/app/assets/javascripts/droplab/droplab.js @@ -108,7 +108,7 @@ Object.assign(DropDown.prototype, { // Help set the image src template var imageTags = template.content.querySelectorAll('img[data-src]'); for(var i = 0; i < imageTags.length; i++) { - var imageTag = imagetags[i]; + var imageTag = imageTags[i]; imageTag.src = imageTag.getAttribute('data-src'); imageTag.removeAttribute('data-src'); } diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index cf5b1a52332..c7841486ad1 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -28,7 +28,7 @@ %ul{ 'data-dynamic' => true } %li %button.btn.btn-link - %img.avatar.avatar-inline{ 'src': '{{avatar_url}}', width: '30' } + %img.avatar.avatar-inline{ 'data-src': '{{avatar_url}}', width: '30' } %strong {{name}} %span @@ -37,7 +37,7 @@ %ul{ 'data-dynamic' => true } %li %button.btn.btn-link - %img.avatar.avatar-inline{ 'src': '{{avatar_url}}', width: '30' } + %img.avatar.avatar-inline{ 'data-src': '{{avatar_url}}', width: '30' } %strong {{name}} %span -- cgit v1.2.1 From f19503b008e962a6eaf16ce4fa18bdb89ceb7442 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 12:26:36 -0600 Subject: Style hint dropdown --- .../filtered_search/dropdown_hint.js.es6 | 4 ++-- app/assets/stylesheets/framework/filters.scss | 22 ++++++++++++++++++++++ app/views/shared/issuable/_search_bar.html.haml | 6 +++--- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index 0593561c8a1..dc28b97fea9 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -32,8 +32,8 @@ } itemClicked(e) { - const token = e.detail.selected.querySelector('.js-filter-hint').innerText.trim(); - const tag = e.detail.selected.querySelector('.js-filter-tag').innerText.trim(); + const token = e.detail.selected.querySelector('.dropdown-filter-hint').innerText.trim(); + const tag = e.detail.selected.querySelector('.dropdown-filter-tag').innerText.trim(); if (tag.length) { gl.FilteredSearchManager.addWordToInput(this.getSelectedText(token)); diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index c679a3833e9..71b33646185 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -68,3 +68,25 @@ } } } + +.dropdown-menu .filter-dropdown { + padding: 0; +} + +.filter-dropdown { + .btn { + border: none; + width: 100%; + text-align: left; + padding: 8px 16px; + + &:hover { + text-decoration: none; + } + } + + .dropdown-filter-tag { + font-size: 14px; + font-weight: 400; + } +} diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index c7841486ad1..6df35b78194 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -17,12 +17,12 @@ %button.clear-search.hidden{ type: 'button' } = icon('times') %ul#js-dropdown-hint.dropdown-menu{ 'data-dynamic' => true } - %li + %li.filter-dropdown %button.btn.btn-link %i.fa{ 'class': '{{icon}}'} - %span.js-filter-hint + %span.dropdown-filter-hint {{hint}} - %span.js-filter-tag + %span.dropdown-filter-tag {{tag}} #js-dropdown-author.dropdown-menu{ 'data-dropdown' => true } %ul{ 'data-dynamic' => true } -- cgit v1.2.1 From fdd1bac91a0554df9a3a25b500877d376fd6b2a0 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 12:35:28 -0600 Subject: Style author dropdown --- .../filtered_search/dropdown_hint.js.es6 | 4 ++-- app/assets/stylesheets/framework/filters.scss | 11 ++++++++++- app/views/shared/issuable/_search_bar.html.haml | 23 +++++++++++----------- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index dc28b97fea9..0593561c8a1 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -32,8 +32,8 @@ } itemClicked(e) { - const token = e.detail.selected.querySelector('.dropdown-filter-hint').innerText.trim(); - const tag = e.detail.selected.querySelector('.dropdown-filter-tag').innerText.trim(); + const token = e.detail.selected.querySelector('.js-filter-hint').innerText.trim(); + const tag = e.detail.selected.querySelector('.js-filter-tag').innerText.trim(); if (tag.length) { gl.FilteredSearchManager.addWordToInput(this.getSelectedText(token)); diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 71b33646185..767803ac1d0 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -85,8 +85,17 @@ } } - .dropdown-filter-tag { + .dropdown-light-content { font-size: 14px; font-weight: 400; } + + .dropdown-user { + display: flex; + } + + .dropdown-user-details { + display: flex; + flex-direction: column; + } } diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 6df35b78194..b83ea6c60d9 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -20,22 +20,23 @@ %li.filter-dropdown %button.btn.btn-link %i.fa{ 'class': '{{icon}}'} - %span.dropdown-filter-hint + %span.js-filter-hint {{hint}} - %span.dropdown-filter-tag + %span.js-filter-tag.dropdown-light-content {{tag}} #js-dropdown-author.dropdown-menu{ 'data-dropdown' => true } %ul{ 'data-dynamic' => true } - %li - %button.btn.btn-link + %li.filter-dropdown + %button.btn.btn-link.dropdown-user %img.avatar.avatar-inline{ 'data-src': '{{avatar_url}}', width: '30' } - %strong - {{name}} - %span - {{username}} + .dropdown-user-details + %span + {{name}} + %span.dropdown-light-content + @{{username}} #js-dropdown-assignee.dropdown-menu{ 'data-dropdown' => true } %ul{ 'data-dynamic' => true } - %li + %li.filter-dropdown %button.btn.btn-link %img.avatar.avatar-inline{ 'data-src': '{{avatar_url}}', width: '30' } %strong @@ -44,12 +45,12 @@ {{username}} #js-dropdown-milestone.dropdown-menu{ 'data-dropdown' => true } %ul{ 'data-dynamic' => true } - %li + %li.filter-dropdown %button.btn.btn-link {{title}} #js-dropdown-label.dropdown-menu{ 'data-dropdown' => true } %ul{ 'data-dynamic' => true } - %li + %li.filter-dropdown %button.btn.btn-link %span.dropdown-label-box{ 'style': 'background: {{color}}'} {{title}} -- cgit v1.2.1 From 7cf322f1d667b6e0d1c74e58a89aa3dbd437a850 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 13:00:18 -0600 Subject: Add blue hover for dropdowns --- app/assets/stylesheets/framework/filters.scss | 2 ++ app/assets/stylesheets/framework/variables.scss | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 767803ac1d0..4fa826c1b76 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -81,6 +81,8 @@ padding: 8px 16px; &:hover { + background-color: $dropdown-hover-color; + color: white; text-decoration: none; } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 3e52c482ece..f3cb3d33d99 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -263,6 +263,11 @@ $dropdown-chevron-size: 10px; $dropdown-toggle-active-border-color: darken($border-color, 14%); +/* +* Filtered Search +*/ +$dropdown-hover-color: #3B86FF; + /* * Buttons */ -- cgit v1.2.1 From eb55cf5007f478a1fdffe029110c27c869f74356 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 13:00:54 -0600 Subject: Add static dropdown list items --- app/views/shared/issuable/_search_bar.html.haml | 26 ++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index b83ea6c60d9..a47140ed0aa 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -35,20 +35,36 @@ %span.dropdown-light-content @{{username}} #js-dropdown-assignee.dropdown-menu{ 'data-dropdown' => true } - %ul{ 'data-dynamic' => true } + %ul %li.filter-dropdown %button.btn.btn-link + No assignee + %li.divider + %ul{ 'data-dynamic' => true } + %li.filter-dropdown + %button.btn.btn-link.dropdown-user %img.avatar.avatar-inline{ 'data-src': '{{avatar_url}}', width: '30' } - %strong - {{name}} - %span - {{username}} + .dropdown-user-details + %span + {{name}} + %span.dropdown-light-content + @{{username}} #js-dropdown-milestone.dropdown-menu{ 'data-dropdown' => true } + %ul + %li.filter-dropdown + %button.btn.btn-link + No milestone + %li.divider %ul{ 'data-dynamic' => true } %li.filter-dropdown %button.btn.btn-link {{title}} #js-dropdown-label.dropdown-menu{ 'data-dropdown' => true } + %ul + %li.filter-dropdown + %button.btn.btn-link + No label + %li.divider %ul{ 'data-dynamic' => true } %li.filter-dropdown %button.btn.btn-link -- cgit v1.2.1 From 9f1c19ce84046de2e85eff1aef7f693a2925bf11 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 13:09:29 -0600 Subject: Style label dropdowns --- app/assets/stylesheets/framework/filters.scss | 2 ++ app/views/shared/issuable/_search_bar.html.haml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 4fa826c1b76..c4b4a56a8b5 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -79,6 +79,8 @@ width: 100%; text-align: left; padding: 8px 16px; + text-overflow: ellipsis; + overflow-y: hidden; &:hover { background-color: $dropdown-hover-color; diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index a47140ed0aa..f076c9c1a75 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -69,7 +69,7 @@ %li.filter-dropdown %button.btn.btn-link %span.dropdown-label-box{ 'style': 'background: {{color}}'} - {{title}} + {{title}} .pull-right - if boards_page #js-boards-seach.issue-boards-search -- cgit v1.2.1 From 52fd1b08caad773b332216c117bdecc25cbd6256 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 13:17:43 -0600 Subject: Add vertical scrolling for dropdowns --- app/assets/stylesheets/framework/filters.scss | 9 +++++++++ app/views/shared/issuable/_search_bar.html.haml | 26 ++++++++++++------------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index c4b4a56a8b5..2efdb537cb3 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -74,6 +74,11 @@ } .filter-dropdown { + max-height: 215px; + overflow-x: scroll; +} + +.filter-dropdown-item { .btn { border: none; width: 100%; @@ -103,3 +108,7 @@ flex-direction: column; } } + +.hint-dropdown { + width: 250px; +} diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index f076c9c1a75..8dda6e99d2d 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -16,8 +16,8 @@ = icon('filter') %button.clear-search.hidden{ type: 'button' } = icon('times') - %ul#js-dropdown-hint.dropdown-menu{ 'data-dynamic' => true } - %li.filter-dropdown + %ul#js-dropdown-hint.dropdown-menu.hint-dropdown{ 'data-dynamic' => true } + %li.filter-dropdown-item %button.btn.btn-link %i.fa{ 'class': '{{icon}}'} %span.js-filter-hint @@ -25,8 +25,8 @@ %span.js-filter-tag.dropdown-light-content {{tag}} #js-dropdown-author.dropdown-menu{ 'data-dropdown' => true } - %ul{ 'data-dynamic' => true } - %li.filter-dropdown + %ul.filter-dropdown{ 'data-dynamic' => true } + %li.filter-dropdown-item %button.btn.btn-link.dropdown-user %img.avatar.avatar-inline{ 'data-src': '{{avatar_url}}', width: '30' } .dropdown-user-details @@ -36,12 +36,12 @@ @{{username}} #js-dropdown-assignee.dropdown-menu{ 'data-dropdown' => true } %ul - %li.filter-dropdown + %li.filter-dropdown-item %button.btn.btn-link No assignee %li.divider - %ul{ 'data-dynamic' => true } - %li.filter-dropdown + %ul.filter-dropdown{ 'data-dynamic' => true } + %li.filter-dropdown-item %button.btn.btn-link.dropdown-user %img.avatar.avatar-inline{ 'data-src': '{{avatar_url}}', width: '30' } .dropdown-user-details @@ -51,22 +51,22 @@ @{{username}} #js-dropdown-milestone.dropdown-menu{ 'data-dropdown' => true } %ul - %li.filter-dropdown + %li.filter-dropdown-item %button.btn.btn-link No milestone %li.divider - %ul{ 'data-dynamic' => true } - %li.filter-dropdown + %ul.filter-dropdown{ 'data-dynamic' => true } + %li.filter-dropdown-item %button.btn.btn-link {{title}} #js-dropdown-label.dropdown-menu{ 'data-dropdown' => true } %ul - %li.filter-dropdown + %li.filter-dropdown-item %button.btn.btn-link No label %li.divider - %ul{ 'data-dynamic' => true } - %li.filter-dropdown + %ul.filter-dropdown{ 'data-dynamic' => true } + %li.filter-dropdown-item %button.btn.btn-link %span.dropdown-label-box{ 'style': 'background: {{color}}'} {{title}} -- cgit v1.2.1 From c0ec94f5f0e0ba24655ab06f60a8da3ccd2930f8 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 13:26:24 -0600 Subject: Fix css dropdown width --- app/assets/stylesheets/framework/filters.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 2efdb537cb3..1f980c3d618 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -69,7 +69,7 @@ } } -.dropdown-menu .filter-dropdown { +.dropdown-menu .filter-dropdown-item { padding: 0; } -- cgit v1.2.1 From 24042d882b544e8909170bdb49d080c5c9e153fd Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 13:32:44 -0600 Subject: Add white background for dropdown label box color --- app/assets/stylesheets/framework/filters.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 1f980c3d618..bcbf0e868e2 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -91,6 +91,12 @@ background-color: $dropdown-hover-color; color: white; text-decoration: none; + + .dropdown-label-box { + border-color: white; + border-style: solid; + border-width: 2px; + } } } -- cgit v1.2.1 From 0e7b8413329270379b6675d87fb5cc28a5b24a58 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 13:34:20 -0600 Subject: Add focus state style the same as hover state --- app/assets/stylesheets/framework/filters.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index bcbf0e868e2..130d06b601c 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -87,7 +87,8 @@ text-overflow: ellipsis; overflow-y: hidden; - &:hover { + &:hover, + &:focus { background-color: $dropdown-hover-color; color: white; text-decoration: none; -- cgit v1.2.1 From e36d32327185e9e8824b04f0cebcbfbffa42d7e8 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 15:12:34 -0600 Subject: Add dropdown offset to match input cursor --- .../filtered_search_dropdown.js.es6 | 4 ++++ .../filtered_search/filtered_search_manager.js.es6 | 24 +++++++++++++++++----- app/assets/javascripts/lib/utils/text_utility.js | 16 +++++++++++++++ 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 index 250d8236ea9..251162f3fb1 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -52,6 +52,10 @@ this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.listId}`); } + setOffset(offset = 0) { + this.dropdown.style.left = `${offset}px`; + } + getCurrentHook() { return droplab.hooks.filter(h => h.id === this.hookId)[0]; } diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index f06d5a646cf..c80c60d6d6e 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -99,47 +99,61 @@ loadDropdown(dropdownName = '') { dropdownName = dropdownName.toLowerCase(); + const filterIconPadding = 27; const match = gl.FilteredSearchTokenKeys.get().filter(value => value.key === dropdownName)[0]; + const filteredSearch = document.querySelector('.filtered-search'); if (match && this.currentDropdown !== match.key) { console.log(`🦄 load ${match.key} dropdown`); + + const dynamicDropdownPadding = 12; + const dropdownOffset = gl.text.getTextWidth(filteredSearch.value) + filterIconPadding + dynamicDropdownPadding; + this.dismissCurrentDropdown(); this.currentDropdown = match.key; if (match.key === 'author') { if (!dropdownAuthor) { - dropdownAuthor = new gl.DropdownAuthor(document.querySelector('#js-dropdown-author'), document.querySelector('.filtered-search')); + dropdownAuthor = new gl.DropdownAuthor(document.querySelector('#js-dropdown-author'), filteredSearch); } + dropdownAuthor.setOffset(dropdownOffset); dropdownAuthor.render(); } else if (match.key === 'assignee') { if (!dropdownAssignee) { - dropdownAssignee = new gl.DropdownAssignee(document.querySelector('#js-dropdown-assignee'), document.querySelector('.filtered-search')); + dropdownAssignee = new gl.DropdownAssignee(document.querySelector('#js-dropdown-assignee'), filteredSearch); } + dropdownAssignee.setOffset(dropdownOffset); dropdownAssignee.render(); } else if (match.key === 'milestone') { if (!dropdownMilestone) { - dropdownMilestone = new gl.DropdownMilestone(document.querySelector('#js-dropdown-milestone'), document.querySelector('.filtered-search')); + dropdownMilestone = new gl.DropdownMilestone(document.querySelector('#js-dropdown-milestone'), filteredSearch); } + dropdownMilestone.setOffset(dropdownOffset); dropdownMilestone.render(); } else if (match.key === 'label') { if (!dropdownLabel) { - dropdownLabel = new gl.DropdownLabel(document.querySelector('#js-dropdown-label'), document.querySelector('.filtered-search')); + dropdownLabel = new gl.DropdownLabel(document.querySelector('#js-dropdown-label'), filteredSearch); } + dropdownLabel.setOffset(dropdownOffset); dropdownLabel.render(); } } else if (!match && this.currentDropdown !== 'hint') { console.log('🦄 load hint dropdown'); + + const dropdownOffset = gl.text.getTextWidth(filteredSearch.value) + filterIconPadding; + this.dismissCurrentDropdown(); this.currentDropdown = 'hint'; if (!dropdownHint) { - dropdownHint = new gl.DropdownHint(document.querySelector('#js-dropdown-hint'), document.querySelector('.filtered-search'), this.currentDropdown); + dropdownHint = new gl.DropdownHint(document.querySelector('#js-dropdown-hint'), filteredSearch, this.currentDropdown); } + dropdownHint.setOffset(dropdownOffset); dropdownHint.render(); } } diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index 5066e3282d7..e47eccc3a33 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -17,6 +17,22 @@ gl.text.replaceRange = function(s, start, end, substitute) { return s.substring(0, start) + substitute + s.substring(end); }; + gl.text.getTextWidth = function(text, font) { + /** + * Uses canvas.measureText to compute and return the width of the given text of given font in pixels. + * + * @param {String} text The text to be rendered. + * @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana"). + * + * @see http://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393 + */ + // re-use canvas object for better performance + var canvas = gl.text.getTextWidth.canvas || (gl.text.getTextWidth.canvas = document.createElement("canvas")); + var context = canvas.getContext("2d"); + context.font = font; + var metrics = context.measureText(text); + return metrics.width; + }; gl.text.selectedText = function(text, textarea) { return text.substring(textarea.selectionStart, textarea.selectionEnd); }; -- cgit v1.2.1 From dd90dd0e28839a0e83a2145aa13c6f517efbee89 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 15:40:55 -0600 Subject: Set data_dropdown_trigger to empty instead of removing --- app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 index 251162f3fb1..0a406bef985 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -42,7 +42,7 @@ } dismissDropdown() { - this.input.removeAttribute(DATA_DROPDOWN_TRIGGER); + this.input.setAttribute(DATA_DROPDOWN_TRIGGER, ''); droplab.setConfig(this.getFilterConfig()); droplab.setData(this.hookId, []); this.unbindEvents(); -- cgit v1.2.1 From d2ecba6edfea8ff1836943b6c683fdb36e4c92a3 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 16:04:06 -0600 Subject: Remove bad droplab code --- app/assets/javascripts/droplab/droplab.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js index 0152eef793f..6befa0976d4 100644 --- a/app/assets/javascripts/droplab/droplab.js +++ b/app/assets/javascripts/droplab/droplab.js @@ -126,9 +126,6 @@ Object.assign(DropDown.prototype, { } else { this.list.innerHTML = newChildren.join(''); } - - // Show dropdown if there is data - data !== [] ? this.show() : this.hide(); }, show: function() { -- cgit v1.2.1 From f5719c2c48dd989ed5431a78e95e560f3c1d9335 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 16:05:30 -0600 Subject: Add ability to click on none as an option --- .../filtered_search/dropdown_assignee.js.es6 | 8 +++++++- .../javascripts/filtered_search/dropdown_hint.js.es6 | 5 ----- .../javascripts/filtered_search/dropdown_label.js.es6 | 9 ++++++++- .../filtered_search/dropdown_milestone.js.es6 | 8 +++++++- .../filtered_search/filtered_search_dropdown.js.es6 | 19 ++++++++++++++++++- .../filtered_search/filtered_search_manager.js.es6 | 2 +- app/views/shared/issuable/_search_bar.html.haml | 9 +++++---- 7 files changed, 46 insertions(+), 14 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 index e3cbb4cb3a0..fcaacac1b50 100644 --- a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 @@ -9,7 +9,13 @@ } itemClicked(e) { - console.log('assignee clicked'); + const dataValueSet = this.setDataValueIfSelected(e.detail.selected); + + if (!dataValueSet) { + console.log('set value'); + } + + this.dismissDropdown(); } renderContent() { diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index 0593561c8a1..b7161d00eb9 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -39,12 +39,7 @@ gl.FilteredSearchManager.addWordToInput(this.getSelectedText(token)); } - this.input.focus(); this.dismissDropdown(); - - // Propogate input change to FilteredSearchManager - // so that it can determine which dropdowns to open - this.input.dispatchEvent(new Event('input')); } renderContent() { diff --git a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 index 9225dca13b0..ef92ecd3bd1 100644 --- a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 @@ -9,7 +9,14 @@ } itemClicked(e) { - console.log('label clicked'); + const dataValueSet = this.setDataValueIfSelected(e.detail.selected); + + if (!dataValueSet) { + const labelName = `~${e.detail.selected.querySelector('.label-title').innerText.trim()}`; + gl.FilteredSearchManager.addWordToInput(this.getSelectedText(labelName)); + } + + this.dismissDropdown(); } renderContent() { diff --git a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 index ab97d709886..00df89ff063 100644 --- a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 @@ -9,7 +9,13 @@ } itemClicked(e) { - console.log('milestone clicked'); + const dataValueSet = this.setDataValueIfSelected(e.detail.selected); + + if (!dataValueSet) { + console.log('set value'); + } + + this.dismissDropdown(); } renderContent() { diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 index 0a406bef985..a345b368238 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -41,13 +41,20 @@ return config; } - dismissDropdown() { + destroy() { this.input.setAttribute(DATA_DROPDOWN_TRIGGER, ''); droplab.setConfig(this.getFilterConfig()); droplab.setData(this.hookId, []); this.unbindEvents(); } + dismissDropdown() { + this.input.focus(); + // Propogate input change to FilteredSearchManager + // so that it can determine which dropdowns to open + this.input.dispatchEvent(new Event('input')); + } + setAsDropdown() { this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.listId}`); } @@ -56,6 +63,16 @@ this.dropdown.style.left = `${offset}px`; } + setDataValueIfSelected(selected) { + const dataValue = selected.getAttribute('data-value'); + + if (dataValue) { + gl.FilteredSearchManager.addWordToInput(dataValue); + } + + return dataValue !== null; + } + getCurrentHook() { return droplab.hooks.filter(h => h.id === this.hookId)[0]; } diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index c80c60d6d6e..53ab2135a09 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -160,7 +160,7 @@ dismissCurrentDropdown() { if (this.currentDropdown === 'hint') { - dropdownHint.dismissDropdown(); + dropdownHint.destroy(); } } diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 8dda6e99d2d..39af0c2c288 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -36,7 +36,7 @@ @{{username}} #js-dropdown-assignee.dropdown-menu{ 'data-dropdown' => true } %ul - %li.filter-dropdown-item + %li.filter-dropdown-item{ 'data-value': 'none' } %button.btn.btn-link No assignee %li.divider @@ -51,7 +51,7 @@ @{{username}} #js-dropdown-milestone.dropdown-menu{ 'data-dropdown' => true } %ul - %li.filter-dropdown-item + %li.filter-dropdown-item{ 'data-value': 'none' } %button.btn.btn-link No milestone %li.divider @@ -61,7 +61,7 @@ {{title}} #js-dropdown-label.dropdown-menu{ 'data-dropdown' => true } %ul - %li.filter-dropdown-item + %li.filter-dropdown-item{ 'data-value': 'none' } %button.btn.btn-link No label %li.divider @@ -69,7 +69,8 @@ %li.filter-dropdown-item %button.btn.btn-link %span.dropdown-label-box{ 'style': 'background: {{color}}'} - {{title}} + %span.label-title + {{title}} .pull-right - if boards_page #js-boards-seach.issue-boards-search -- cgit v1.2.1 From 0c1c26c0bcc5c62f5959d7fc1399d44e1a6617cd Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 16:29:55 -0600 Subject: Replace typed token with selected dropdown token --- .../javascripts/filtered_search/filtered_search_manager.js.es6 | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 53ab2135a09..7e6144b571d 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -92,7 +92,14 @@ } static addWordToInput(word, addSpace) { - const hasExistingValue = document.querySelector('.filtered-search').value.length !== 0; + const filteredSearchValue = document.querySelector('.filtered-search').value; + const hasExistingValue = filteredSearchValue.length !== 0; + + const { lastToken } = gl.FilteredSearchTokenizer.processTokens(filteredSearchValue); + if (lastToken.hasOwnProperty('key')) { + document.querySelector('.filtered-search').value = filteredSearchValue.slice(0, -1 * (lastToken.value.length)); + } + document.querySelector('.filtered-search').value += hasExistingValue && addSpace ? ` ${word}` : word; } -- cgit v1.2.1 From 71da8ffaef9220c10d2b95ca0ae06bc08fefa594 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 16:36:05 -0600 Subject: Populate selected item in filtered search input --- app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 | 3 ++- app/assets/javascripts/filtered_search/dropdown_author.js.es6 | 5 ++++- app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 | 3 ++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 index fcaacac1b50..e791de5ad41 100644 --- a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 @@ -12,7 +12,8 @@ const dataValueSet = this.setDataValueIfSelected(e.detail.selected); if (!dataValueSet) { - console.log('set value'); + const username = e.detail.selected.querySelector('.dropdown-light-content').innerText.trim(); + gl.FilteredSearchManager.addWordToInput(this.getSelectedText(username)); } this.dismissDropdown(); diff --git a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 index e16b313b743..75eb1c06fbd 100644 --- a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 @@ -9,7 +9,10 @@ } itemClicked(e) { - console.log('author clicked'); + const username = e.detail.selected.querySelector('.dropdown-light-content').innerText.trim(); + gl.FilteredSearchManager.addWordToInput(this.getSelectedText(username)); + + this.dismissDropdown(); } renderContent() { diff --git a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 index 00df89ff063..8c75bd30e97 100644 --- a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 @@ -12,7 +12,8 @@ const dataValueSet = this.setDataValueIfSelected(e.detail.selected); if (!dataValueSet) { - console.log('set value'); + const milestoneName = `%${e.detail.selected.querySelector('.btn-link').innerText.trim()}`; + gl.FilteredSearchManager.addWordToInput(this.getSelectedText(milestoneName)); } this.dismissDropdown(); -- cgit v1.2.1 From bcae21b1badaa3a7aedfc44f67908909b34afb7d Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 6 Dec 2016 11:34:00 -0600 Subject: Remove border radius of list item buttons --- app/assets/stylesheets/framework/filters.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 130d06b601c..0882af57482 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -86,6 +86,7 @@ padding: 8px 16px; text-overflow: ellipsis; overflow-y: hidden; + border-radius: 0; &:hover, &:focus { -- cgit v1.2.1 From ab2808531dcd768a03f4a9fd90ec0ca67d013278 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 6 Dec 2016 11:37:54 -0600 Subject: Remove unnecessary dismissCurrentDropdown --- .../javascripts/filtered_search/filtered_search_manager.js.es6 | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 7e6144b571d..c509a3c3b62 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -154,7 +154,6 @@ const dropdownOffset = gl.text.getTextWidth(filteredSearch.value) + filterIconPadding; - this.dismissCurrentDropdown(); this.currentDropdown = 'hint'; if (!dropdownHint) { @@ -165,12 +164,6 @@ } } - dismissCurrentDropdown() { - if (this.currentDropdown === 'hint') { - dropdownHint.destroy(); - } - } - setDropdown() { const { lastToken } = this.tokenizer.processTokens(document.querySelector('.filtered-search').value); -- cgit v1.2.1 From 60c9240bc27ea1c5de1acb2a4cc686d9e2a85555 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 6 Dec 2016 12:46:16 -0600 Subject: Fixed bug where labels with multiple spaces wouldn't get tokenized correctly --- .../javascripts/filtered_search/filtered_search_tokenizer.es6 | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 index b686a43cf32..17fdfe0f550 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 @@ -40,18 +40,21 @@ let lastQuotation = ''; let incompleteToken = false; + // Iterate through each word (broken up by spaces) inputs.forEach((i) => { if (incompleteToken) { + // Continue previous token as it had an escaped + // quote in the beginning const prevToken = tokens.last(); prevToken.value += ` ${i}`; - // Remove last quotation + // Remove last quotation from the value const lastQuotationRegex = new RegExp(lastQuotation, 'g'); prevToken.value = prevToken.value.replace(lastQuotationRegex, ''); tokens[tokens.length - 1] = prevToken; // Check to see if this quotation completes the token value - if (i.indexOf(lastQuotation)) { + if (i.indexOf(lastQuotation) !== -1) { lastToken = tokens.last(); incompleteToken = !incompleteToken; } -- cgit v1.2.1 From da8ab2bc086b6b7c2775d44198a6b3bc04794c3d Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 6 Dec 2016 12:52:23 -0600 Subject: Add escape quotations for selected labels from dropdown --- .../javascripts/filtered_search/dropdown_label.js.es6 | 18 ++++++++++++++++-- .../filtered_search/filtered_search_manager.js.es6 | 7 ++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 index ef92ecd3bd1..cd1ccb541e6 100644 --- a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 @@ -12,8 +12,22 @@ const dataValueSet = this.setDataValueIfSelected(e.detail.selected); if (!dataValueSet) { - const labelName = `~${e.detail.selected.querySelector('.label-title').innerText.trim()}`; - gl.FilteredSearchManager.addWordToInput(this.getSelectedText(labelName)); + let labelTitle = e.detail.selected.querySelector('.label-title').innerText.trim(); + + // Encapsulate label with quotes if it has spaces + if (labelTitle.indexOf(' ') !== -1) { + if (labelTitle.indexOf('"') !== -1) { + // Use single quotes if label title contains double quotes + labelTitle = `'${labelTitle}'`; + } else { + // Known side effect: Label's with both single and double quotes + // won't escape properly + labelTitle = `"${labelTitle}"`; + } + } + + const labelName = `~${labelTitle}`; + gl.FilteredSearchManager.addWordToInput(labelName); } this.dismissDropdown(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index c509a3c3b62..04374525d4c 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -97,7 +97,12 @@ const { lastToken } = gl.FilteredSearchTokenizer.processTokens(filteredSearchValue); if (lastToken.hasOwnProperty('key')) { - document.querySelector('.filtered-search').value = filteredSearchValue.slice(0, -1 * (lastToken.value.length)); + console.log(lastToken); + // Spaces inside the token means that the token value will be escaped by quotes + const hasQuotes = lastToken.value.indexOf(' ') !== -1; + + const lengthToRemove = hasQuotes ? lastToken.value.length + 2 : lastToken.value.length; + document.querySelector('.filtered-search').value = filteredSearchValue.slice(0, -1 * (lengthToRemove)); } document.querySelector('.filtered-search').value += hasExistingValue && addSpace ? ` ${word}` : word; -- cgit v1.2.1 From 5cd90ef9489797c858bfb7eec7be0f5773d4d417 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 6 Dec 2016 12:57:07 -0600 Subject: Add escaping for milestone values --- .../javascripts/filtered_search/dropdown_label.js.es6 | 17 ++--------------- .../filtered_search/dropdown_milestone.js.es6 | 3 ++- .../filtered_search/filtered_search_dropdown.js.es6 | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 index cd1ccb541e6..d4a50422c3b 100644 --- a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 @@ -12,21 +12,8 @@ const dataValueSet = this.setDataValueIfSelected(e.detail.selected); if (!dataValueSet) { - let labelTitle = e.detail.selected.querySelector('.label-title').innerText.trim(); - - // Encapsulate label with quotes if it has spaces - if (labelTitle.indexOf(' ') !== -1) { - if (labelTitle.indexOf('"') !== -1) { - // Use single quotes if label title contains double quotes - labelTitle = `'${labelTitle}'`; - } else { - // Known side effect: Label's with both single and double quotes - // won't escape properly - labelTitle = `"${labelTitle}"`; - } - } - - const labelName = `~${labelTitle}`; + const labelTitle = e.detail.selected.querySelector('.label-title').innerText.trim(); + const labelName = `~${this.getEscapedText(labelTitle)}`; gl.FilteredSearchManager.addWordToInput(labelName); } diff --git a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 index 8c75bd30e97..965a8c8a58d 100644 --- a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 @@ -12,7 +12,8 @@ const dataValueSet = this.setDataValueIfSelected(e.detail.selected); if (!dataValueSet) { - const milestoneName = `%${e.detail.selected.querySelector('.btn-link').innerText.trim()}`; + const milestoneTitle = e.detail.selected.querySelector('.btn-link').innerText.trim(); + const milestoneName = `%${this.getEscapedText(milestoneTitle)}`; gl.FilteredSearchManager.addWordToInput(this.getSelectedText(milestoneName)); } diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 index a345b368238..cc7f61c23e5 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -18,6 +18,24 @@ this.dropdown.removeEventListener('click.dl', this.itemClicked.bind(this)); } + getEscapedText(text) { + let escapedText = text; + + // Encapsulate value with quotes if it has spaces + if (text.indexOf(' ') !== -1) { + if (text.indexOf('"') !== -1) { + // Use single quotes if value contains double quotes + escapedText = `'${text}'`; + } else { + // Known side effect: values's with both single and double quotes + // won't escape properly + escapedText = `"${text}"`; + } + } + + return escapedText; + } + getSelectedText(selectedToken) { // TODO: Get last word from FilteredSearchTokenizer const lastWord = this.input.value.split(' ').last(); -- cgit v1.2.1 From 00ed5aafd227a7d4f9be6448f1bcdacff6ee6dfa Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 6 Dec 2016 15:13:31 -0600 Subject: Make keep typing dropdown item static --- .../filtered_search/dropdown_hint.js.es6 | 4 ---- app/views/shared/issuable/_search_bar.html.haml | 23 ++++++++++++++-------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index b7161d00eb9..b09136586c8 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -3,10 +3,6 @@ ((global) => { const dropdownData = [{ - icon: 'fa-search', - hint: 'Keep typing and press Enter', - tag: '', - },{ icon: 'fa-pencil', hint: 'author:', tag: '<author>' diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 39af0c2c288..0a5de59cb63 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -16,14 +16,21 @@ = icon('filter') %button.clear-search.hidden{ type: 'button' } = icon('times') - %ul#js-dropdown-hint.dropdown-menu.hint-dropdown{ 'data-dynamic' => true } - %li.filter-dropdown-item - %button.btn.btn-link - %i.fa{ 'class': '{{icon}}'} - %span.js-filter-hint - {{hint}} - %span.js-filter-tag.dropdown-light-content - {{tag}} + #js-dropdown-hint.dropdown-menu.hint-dropdown + %ul + %li.filter-dropdown-item{ 'data-value': 'none' } + %button.btn.btn-link + = icon('search') + %span + Keep typing and press Enter + %ul.filter-dropdown{ 'data-dynamic' => true } + %li.filter-dropdown-item + %button.btn.btn-link + %i.fa{ 'class': '{{icon}}'} + %span.js-filter-hint + {{hint}} + %span.js-filter-tag.dropdown-light-content + {{tag}} #js-dropdown-author.dropdown-menu{ 'data-dropdown' => true } %ul.filter-dropdown{ 'data-dynamic' => true } %li.filter-dropdown-item -- cgit v1.2.1 From d91d586aa60c8194a675e936832667cfe12729be Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 6 Dec 2016 16:12:00 -0600 Subject: Add filter by last token --- app/assets/javascripts/droplab/droplab_filter.js | 13 +++++- .../filtered_search/dropdown_author.js.es6 | 8 ++++ .../filtered_search_dropdown.js.es6 | 13 ++++-- .../filtered_search/filtered_search_tokenizer.es6 | 49 ++++++++++++++++++++++ 4 files changed, 77 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab_filter.js b/app/assets/javascripts/droplab/droplab_filter.js index 88e69c02422..5ae81afaf89 100644 --- a/app/assets/javascripts/droplab/droplab_filter.js +++ b/app/assets/javascripts/droplab/droplab_filter.js @@ -10,13 +10,22 @@ droplab.plugin(function init(DropLab) { var matches = []; // will only work on dynamically set data // and if a config text property is set - if(!data || !config.hasOwnProperty('text')){ + if(!data || (!config.hasOwnProperty('text') && !config.hasOwnProperty('filter'))){ return; } - matches = data.map(function(o){ + + var filterFunction = function(o){ // cheap string search o.droplab_hidden = o[config.text].toLowerCase().indexOf(value) === -1; return o; + }; + + if (config.hasOwnProperty('filter') && config.filter !== undefined) { + filterFunction = config.filter; + } + + matches = data.map(function(o) { + return filterFunction(o, value); }); list.render(matches); } diff --git a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 index 75eb1c06fbd..64c310ba7ad 100644 --- a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 @@ -19,6 +19,14 @@ super.renderContent(); droplab.setData(this.hookId, '/autocomplete/users.json?search=&per_page=20&active=true&project_id=2&group_id=&skip_ldap=&todo_filter=&todo_state_filter=¤t_user=true&push_code_to_protected_branches=&author_id=&skip_users='); } + + filterMethod(item, query) { + const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); + const valueWithoutPrefix = value.slice(1); + + item.droplab_hidden = item['username'].indexOf(valueWithoutPrefix) === -1; + return item; + } } global.DropdownAuthor = DropdownAuthor; diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 index cc7f61c23e5..bbfe26e6a21 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -50,12 +50,17 @@ getFilterConfig(filterKeyword) { const config = {}; - const filterConfig = { - text: filterKeyword, - }; + const filterConfig = {}; - config[this.hookId] = filterKeyword ? filterConfig : {}; + if (filterKeyword) { + filterConfig.text = filterKeyword; + } + + if (this.filterMethod) { + filterConfig.filter = this.filterMethod; + } + config[this.hookId] = filterConfig; return config; } diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 index 17fdfe0f550..d6df83a3fb9 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 @@ -29,6 +29,55 @@ } } + static getLastTokenObject(input) { + const token = FilteredSearchTokenizer.getLastToken(input); + const colonIndex = token.indexOf(':'); + + const key = colonIndex !== -1 ? token.slice(0, colonIndex) : ''; + const value = colonIndex !== -1 ? token.slice(colonIndex) : token; + + return { + key, + value, + } + } + + static getLastToken(input) { + let completeToken = false; + let completeQuotation = true; + let lastQuotation = ''; + let i = input.length; + + const doubleQuote = '"'; + const singleQuote = '\''; + while(!completeToken && i >= 0) { + const isDoubleQuote = input[i] === doubleQuote; + const isSingleQuote = input[i] === singleQuote; + + // If the second quotation is found + if ((lastQuotation === doubleQuote && input[i] === doubleQuote) || + (lastQuotation === singleQuote && input[i] === singleQuote)) { + completeQuotation = true; + } + + // Save the first quotation + if ((input[i] === doubleQuote && lastQuotation === '') || + (input[i] === singleQuote && lastQuotation === '')) { + lastQuotation = input[i]; + completeQuotation = false; + } + + if (completeQuotation && input[i] === ' ') { + completeToken = true; + } else { + i--; + } + } + + // Adjust by 1 because of empty space + return input.slice(i + 1); + } + static processTokens(input) { let tokens = []; let searchToken = ''; -- cgit v1.2.1 From 6d2d2b2bd1448bb46b586d5dbe0edc88f020967c Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 6 Dec 2016 16:20:14 -0600 Subject: Add filtering to the remaining dropdowns --- app/assets/javascripts/filtered_search/dropdown_label.js.es6 | 8 ++++++++ app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 index d4a50422c3b..c5493f7a887 100644 --- a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 @@ -24,6 +24,14 @@ super.renderContent(); droplab.setData(this.hookId, 'labels.json'); } + + filterMethod(item, query) { + const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); + const valueWithoutPrefix = value.slice(1); + + item.droplab_hidden = item['title'].indexOf(valueWithoutPrefix) === -1; + return item; + } } global.DropdownLabel = DropdownLabel; diff --git a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 index 965a8c8a58d..8317ce5824c 100644 --- a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 @@ -24,6 +24,14 @@ super.renderContent(); droplab.setData(this.hookId, 'milestones.json'); } + + filterMethod(item, query) { + const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); + const valueWithoutPrefix = value.slice(1); + + item.droplab_hidden = item['title'].indexOf(valueWithoutPrefix) === -1; + return item; + } } global.DropdownMilestone = DropdownMilestone; -- cgit v1.2.1 From b16a38c8a2c9b1090e3b28c47516e9161130ee10 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 6 Dec 2016 16:23:04 -0600 Subject: Add filterMethod to hint dropdown --- app/assets/javascripts/filtered_search/dropdown_hint.js.es6 | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index b09136586c8..0bee2eb2986 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -42,6 +42,18 @@ super.renderContent(); droplab.setData(this.hookId, dropdownData); } + + filterMethod(item, query) { + const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); + + if (value === '') { + item.droplab_hidden = false; + } else { + item.droplab_hidden = item['hint'].indexOf(value) === -1; + } + + return item; + } } global.DropdownHint = DropdownHint; -- cgit v1.2.1 From 6d2a8b5b140a26f5e83ae7b6497cf14bc9a065ee Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 7 Dec 2016 12:33:02 -0600 Subject: Add font to dropdown offset calculation --- .../filtered_search/filtered_search_manager.js.es6 | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 04374525d4c..7e399427cef 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -108,18 +108,22 @@ document.querySelector('.filtered-search').value += hasExistingValue && addSpace ? ` ${word}` : word; } - loadDropdown(dropdownName = '') { + loadDropdown(dropdownName = '', hideDropdown) { dropdownName = dropdownName.toLowerCase(); const filterIconPadding = 27; const match = gl.FilteredSearchTokenKeys.get().filter(value => value.key === dropdownName)[0]; const filteredSearch = document.querySelector('.filtered-search'); + if (!this.font) { + this.font = window.getComputedStyle(filteredSearch).font; + } + if (match && this.currentDropdown !== match.key) { console.log(`🦄 load ${match.key} dropdown`); const dynamicDropdownPadding = 12; - const dropdownOffset = gl.text.getTextWidth(filteredSearch.value) + filterIconPadding + dynamicDropdownPadding; + const dropdownOffset = gl.text.getTextWidth(filteredSearch.value, this.font) + filterIconPadding + dynamicDropdownPadding; this.dismissCurrentDropdown(); this.currentDropdown = match.key; @@ -157,8 +161,9 @@ } else if (!match && this.currentDropdown !== 'hint') { console.log('🦄 load hint dropdown'); - const dropdownOffset = gl.text.getTextWidth(filteredSearch.value) + filterIconPadding; - + const dropdownOffset = gl.text.getTextWidth(filteredSearch.value, this.font) + filterIconPadding; + console.log(dropdownOffset) + this.dismissCurrentDropdown(); this.currentDropdown = 'hint'; if (!dropdownHint) { -- cgit v1.2.1 From 158e90d13527897b2bdd49733c537898a387f2cb Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 7 Dec 2016 12:33:33 -0600 Subject: Add padding for clear button --- app/assets/stylesheets/framework/filters.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 0882af57482..205cecb4906 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -36,6 +36,7 @@ .form-control { padding-left: 25px; + padding-right: 25px; &:focus ~ .fa-filter { color: #444; -- cgit v1.2.1 From 15454eb503bda8ffa9579e08ebe3f3f7114b7643 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 7 Dec 2016 12:51:25 -0600 Subject: Add ability to search for filter dropdowns without filter symbol --- .../javascripts/filtered_search/dropdown_assignee.js.es6 | 15 +++++++++++++++ .../javascripts/filtered_search/dropdown_author.js.es6 | 11 +++++++++-- .../javascripts/filtered_search/dropdown_label.js.es6 | 8 ++++++-- .../javascripts/filtered_search/dropdown_milestone.js.es6 | 9 +++++++-- 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 index e791de5ad41..63fbe30ee84 100644 --- a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 @@ -23,6 +23,21 @@ super.renderContent(); droplab.setData(this.hookId, '/autocomplete/users.json?search=&per_page=20&active=true&project_id=2&group_id=&skip_ldap=&todo_filter=&todo_state_filter=¤t_user=true&push_code_to_protected_branches=&author_id=&skip_users='); } + + filterMethod(item, query) { + const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); + const valueWithoutColon = value.slice(1).toLowerCase(); + const valueWithoutPrefix = valueWithoutColon.slice(1); + + const username = item.username.toLowerCase(); + const name = item.name.toLowerCase(); + + const noUsernameMatch = username.indexOf(valueWithoutPrefix) === -1 && username.indexOf(valueWithoutColon) === -1; + const noNameMatch = name.indexOf(valueWithoutColon) === -1; + + item.droplab_hidden = noUsernameMatch && noNameMatch; + return item; + } } global.DropdownAssignee = DropdownAssignee; diff --git a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 index 64c310ba7ad..37e2e80533b 100644 --- a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 @@ -22,9 +22,16 @@ filterMethod(item, query) { const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); - const valueWithoutPrefix = value.slice(1); + const valueWithoutColon = value.slice(1).toLowerCase(); + const valueWithoutPrefix = valueWithoutColon.slice(1); - item.droplab_hidden = item['username'].indexOf(valueWithoutPrefix) === -1; + const username = item.username.toLowerCase(); + const name = item.name.toLowerCase(); + + const noUsernameMatch = username.indexOf(valueWithoutPrefix) === -1 && username.indexOf(valueWithoutColon) === -1; + const noNameMatch = name.indexOf(valueWithoutColon) === -1; + + item.droplab_hidden = noUsernameMatch && noNameMatch; return item; } } diff --git a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 index c5493f7a887..e2c1305597a 100644 --- a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 @@ -27,9 +27,13 @@ filterMethod(item, query) { const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); - const valueWithoutPrefix = value.slice(1); + const valueWithoutColon = value.slice(1).toLowerCase(); + const valueWithoutPrefix = valueWithoutColon.slice(1); - item.droplab_hidden = item['title'].indexOf(valueWithoutPrefix) === -1; + const title = item.title.toLowerCase(); + const noTitleMatch = title.indexOf(valueWithoutPrefix) === -1 && title.indexOf(valueWithoutColon) === -1; + + item.droplab_hidden = noTitleMatch; return item; } } diff --git a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 index 8317ce5824c..cd185d31917 100644 --- a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 @@ -27,9 +27,14 @@ filterMethod(item, query) { const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); - const valueWithoutPrefix = value.slice(1); + const valueWithoutColon = value.slice(1).toLowerCase(); + const valueWithoutPrefix = valueWithoutColon.slice(1); - item.droplab_hidden = item['title'].indexOf(valueWithoutPrefix) === -1; + const title = item.title.toLowerCase(); + + const noTitleMatch = title.indexOf(valueWithoutPrefix) === -1 && title.indexOf(valueWithoutColon) === -1; + + item.droplab_hidden = noTitleMatch; return item; } } -- cgit v1.2.1 From bbad61b97c483a2d4e2a153c2e1b10d21edaa1e0 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 7 Dec 2016 12:54:29 -0600 Subject: Fix Droplab --- app/assets/javascripts/droplab/droplab.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js index 6befa0976d4..84cd89297ff 100644 --- a/app/assets/javascripts/droplab/droplab.js +++ b/app/assets/javascripts/droplab/droplab.js @@ -212,7 +212,8 @@ require('./window')(function(w){ var self = this; window.addEventListener('click', function(e){ var thisTag = e.target; - if(thisTag.tagName === 'LI' || thisTag.tagName === 'A'){ + if(thisTag.tagName === 'LI' || thisTag.tagName === 'A' + || thisTag.tagName === 'BUTTON'){ // climb up the tree to find the UL thisTag = utils.closest(thisTag, 'UL'); } @@ -556,7 +557,7 @@ var camelize = function(str) { }; var closest = function(thisTag, stopTag) { - while(thisTag.tagName !== stopTag && thisTag.tagName !== 'HTML'){ + while(thisTag !== null && thisTag.tagName !== stopTag && thisTag.tagName !== 'HTML'){ thisTag = thisTag.parentNode; } return thisTag; -- cgit v1.2.1 From ceb79e3c3cb595ac851fb32c99264c1f43dd9c75 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 7 Dec 2016 12:55:03 -0600 Subject: Reset filters after clear search --- .../filtered_search_dropdown.js.es6 | 38 +++++++++++++++++++++- .../filtered_search/filtered_search_manager.js.es6 | 29 ++++++++++------- 2 files changed, 55 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 index bbfe26e6a21..edffd7fb8e2 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -71,6 +71,20 @@ this.unbindEvents(); } + show() { + const currentHook = this.getCurrentHook(); + if (currentHook) { + currentHook.list.show(); + } + } + + hide() { + const currentHook = this.getCurrentHook(); + if (currentHook) { + currentHook.list.hide(); + } + } + dismissDropdown() { this.input.focus(); // Propogate input change to FilteredSearchManager @@ -104,7 +118,7 @@ droplab.setConfig(this.getFilterConfig(this.filterKeyword)); } - render() { + render(hide) { this.setAsDropdown(); const firstTimeInitialized = this.getCurrentHook() === undefined; @@ -115,6 +129,28 @@ droplab.changeHookList(this.hookId, `#${this.listId}`); this.renderContent(); } + + if (hide) { + this.hide(); + } else { + this.show(); + } + } + + resetFilters() { + const currentHook = this.getCurrentHook(); + + if (currentHook) { + const list = currentHook.list; + + if (list.data) { + const data = list.data.map((item) => { + item.droplab_hidden = false; + }); + + list.render(data); + } + } } } diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 7e399427cef..841738ff627 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -1,13 +1,5 @@ /* eslint-disable no-param-reassign */ ((global) => { - function clearSearch(e) { - e.stopPropagation(); - e.preventDefault(); - - document.querySelector('.filtered-search').value = ''; - document.querySelector('.clear-search').classList.add('hidden'); - } - function toggleClearSearchButton(e) { const clearSearchButton = document.querySelector('.clear-search'); @@ -170,7 +162,13 @@ dropdownHint = new gl.DropdownHint(document.querySelector('#js-dropdown-hint'), filteredSearch, this.currentDropdown); } dropdownHint.setOffset(dropdownOffset); - dropdownHint.render(); + dropdownHint.render(hideDropdown); + } + } + + dismissCurrentDropdown() { + if (this.currentDropdown === 'hint') { + dropdownHint.destroy(); } } @@ -198,7 +196,17 @@ filteredSearchInput.addEventListener('input', this.setDropdown.bind(this)); filteredSearchInput.addEventListener('input', toggleClearSearchButton); filteredSearchInput.addEventListener('keydown', this.checkForEnter.bind(this)); - document.querySelector('.clear-search').addEventListener('click', clearSearch); + document.querySelector('.clear-search').addEventListener('click', this.clearSearch.bind(this)); + } + + clearSearch(e) { + e.stopPropagation(); + e.preventDefault(); + + document.querySelector('.filtered-search').value = ''; + document.querySelector('.clear-search').classList.add('hidden'); + dropdownHint.resetFilters(); + this.loadDropdown('hint', true); } checkDropdownToken(e) { @@ -208,7 +216,6 @@ // Check for dropdown token if (lastToken[lastToken.length - 1] === ':') { const token = lastToken.slice(0, -1); - } } -- cgit v1.2.1 From 367a15882a07a2f48dd7887fea642baf3920b6b7 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 8 Dec 2016 15:36:54 -0600 Subject: Update droplab --- app/assets/javascripts/droplab/droplab.js | 190 ++++++++++++++------- app/assets/javascripts/droplab/droplab_ajax.js | 89 +++++----- app/assets/javascripts/droplab/droplab_filter.js | 77 ++++++--- .../filtered_search/dropdown_assignee.js.es6 | 13 +- .../filtered_search/dropdown_author.js.es6 | 13 +- .../filtered_search/dropdown_hint.js.es6 | 22 ++- .../filtered_search/dropdown_label.js.es6 | 30 ++-- .../filtered_search/dropdown_milestone.js.es6 | 30 ++-- .../filtered_search_dropdown.js.es6 | 60 ++++--- .../filtered_search/filtered_search_manager.js.es6 | 42 +++-- 10 files changed, 354 insertions(+), 212 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js index 84cd89297ff..4d83b609a73 100644 --- a/app/assets/javascripts/droplab/droplab.js +++ b/app/assets/javascripts/droplab/droplab.js @@ -53,6 +53,7 @@ Object.assign(DropDown.prototype, { this.list.addEventListener('click', function(e) { // climb up the tree to find the LI var selected = utils.closest(e.target, 'LI'); + if(selected) { e.preventDefault(); self.hide(); @@ -158,17 +159,22 @@ require('./window')(function(w){ this.ready = false; this.hooks = []; this.queuedData = []; - this.plugins = []; this.config = {}; + this.loadWrapper; if(typeof hook !== 'undefined'){ this.addHook(hook); } - this.addEvents(); }; + Object.assign(DropLab.prototype, { - plugin: function (plugin) { - this.plugins.push(plugin) + load: function() { + this.loadWrapper(); + }, + + loadWrapper: function(){ + var dropdownTriggers = [].slice.apply(document.querySelectorAll('['+DATA_TRIGGER+']')); + this.addHooks(dropdownTriggers).init(); }, addData: function () { @@ -181,6 +187,14 @@ require('./window')(function(w){ this.applyArgs(args, '_setData'); }, + destroy: function() { + this.hooks.forEach(function(h){ + h.destroy(); + }); + this.hooks = []; + this.removeEvents(); + }, + applyArgs: function(args, methodName) { if(this.ready) { this[methodName].apply(this, args); @@ -210,7 +224,7 @@ require('./window')(function(w){ addEvents: function() { var self = this; - window.addEventListener('click', function(e){ + this.windowClickedWrapper = function(e){ var thisTag = e.target; if(thisTag.tagName === 'LI' || thisTag.tagName === 'A' || thisTag.tagName === 'BUTTON'){ @@ -222,10 +236,16 @@ require('./window')(function(w){ self.hooks.forEach(function(hook) { hook.list.hide(); }); - }); + }.bind(this); + w.addEventListener('click', this.windowClickedWrapper); + }, + + removeEvents: function(){ + w.removeEventListener('click', this.windowClickedWrapper); + w.removeEventListener('load', this.loadWrapper); }, - changeHookList: function(trigger, list) { + changeHookList: function(trigger, list, plugins, config) { trigger = document.querySelector('[data-id="'+trigger+'"]'); list = document.querySelector(list); this.hooks.every(function(hook, i) { @@ -234,19 +254,16 @@ require('./window')(function(w){ hook.list.list.innerHTML = hook.list.initialState; hook.list.hide(); - hook.trigger.removeEventListener('mousedown', hook.events.mousedown); - hook.trigger.removeEventListener('input', hook.events.input); - hook.trigger.removeEventListener('keyup', hook.events.keyup); - hook.trigger.removeEventListener('keydown', hook.events.keydown); + hook.destroy(); this.hooks.splice(i, 1); - this.addHook(trigger, list); + this.addHook(trigger, list, plugins, config); return false; } return true }.bind(this)); }, - addHook: function(hook, list) { + addHook: function(hook, list, plugins, config) { if(!(hook instanceof HTMLElement) && typeof hook === 'string'){ hook = document.querySelector(hook); } @@ -256,17 +273,17 @@ require('./window')(function(w){ if(hook) { if(hook.tagName === 'A' || hook.tagName === 'BUTTON') { - this.hooks.push(new HookButton(hook, list)); + this.hooks.push(new HookButton(hook, list, plugins, config)); } else if(hook.tagName === 'INPUT') { - this.hooks.push(new HookInput(hook, list)); + this.hooks.push(new HookInput(hook, list, plugins, config)); } } return this; }, - addHooks: function(hooks) { + addHooks: function(hooks, plugins, config) { hooks.forEach(function(hook) { - this.addHook(hook, null); + this.addHook(hook, null, plugins, config); }.bind(this)); return this; }, @@ -276,9 +293,7 @@ require('./window')(function(w){ }, init: function () { - this.plugins.forEach(function(plugin) { - plugin(DropLab); - }) + this.addEvents(); var readyEvent = new CustomEvent('ready.dl', { detail: { dropdown: this, @@ -301,15 +316,18 @@ require('./window')(function(w){ },{"./constants":1,"./custom_event_polyfill":2,"./hook_button":6,"./hook_input":7,"./utils":10,"./window":11}],5:[function(require,module,exports){ var DropDown = require('./dropdown'); -var Hook = function(trigger, list){ +var Hook = function(trigger, list, plugins, config){ this.trigger = trigger; this.list = new DropDown(list); this.type = 'Hook'; this.event = 'click'; + this.plugins = plugins || []; + this.config = config || {}; this.id = trigger.dataset.id; }; Object.assign(Hook.prototype, { + addEvents: function(){}, constructor: Hook, @@ -321,31 +339,61 @@ module.exports = Hook; var CustomEvent = require('./custom_event_polyfill'); var Hook = require('./hook'); -var HookButton = function(trigger, list) { - Hook.call(this, trigger, list); +var HookButton = function(trigger, list, plugins, config) { + Hook.call(this, trigger, list, plugins, config); this.type = 'button'; this.event = 'click'; this.addEvents(); + this.addPlugins(); }; HookButton.prototype = Object.create(Hook.prototype); Object.assign(HookButton.prototype, { + addPlugins: function() { + this.plugins.forEach(function(plugin) { + plugin.init(this); + }); + }, + + clicked: function(e){ + var buttonEvent = new CustomEvent('click.dl', { + detail: { + hook: this, + }, + bubbles: true, + cancelable: true + }); + this.list.show(); + e.target.dispatchEvent(buttonEvent); + }, + addEvents: function(){ - var self = this; - this.trigger.addEventListener('click', function(e){ - var buttonEvent = new CustomEvent('click.dl', { - detail: { - hook: self, - }, - bubbles: true, - cancelable: true - }); - self.list.show(); - e.target.dispatchEvent(buttonEvent); + this.clickedWrapper = this.clicked.bind(this); + this.trigger.addEventListener('click', this.clickedWrapper); + }, + + removeEvents: function(){ + this.trigger.removeEventListener('click', this.clickedWrapper); + }, + + restoreInitialState: function() { + this.list.list.innerHTML = this.list.initialState; + }, + + removePlugins: function() { + this.plugins.forEach(function(plugin) { + plugin.destroy(); }); }, + destroy: function() { + this.restoreInitialState(); + this.removeEvents(); + this.removePlugins(); + }, + + constructor: HookButton, }); @@ -356,18 +404,26 @@ module.exports = HookButton; var CustomEvent = require('./custom_event_polyfill'); var Hook = require('./hook'); -var HookInput = function(trigger, list) { - Hook.call(this, trigger, list); +var HookInput = function(trigger, list, plugins, config) { + Hook.call(this, trigger, list, plugins, config); this.type = 'input'; this.event = 'input'; + this.addPlugins(); this.addEvents(); }; Object.assign(HookInput.prototype, { + addPlugins: function() { + var self = this; + this.plugins.forEach(function(plugin) { + plugin.init(self); + }); + }, + addEvents: function(){ var self = this; - function mousedown(e) { + this.mousedown = function mousedown(e) { var mouseEvent = new CustomEvent('mousedown.dl', { detail: { hook: self, @@ -379,7 +435,7 @@ Object.assign(HookInput.prototype, { e.target.dispatchEvent(mouseEvent); } - function input(e) { + this.input = function input(e) { var inputEvent = new CustomEvent('input.dl', { detail: { hook: self, @@ -392,11 +448,11 @@ Object.assign(HookInput.prototype, { self.list.show(); } - function keyup(e) { + this.keyup = function keyup(e) { keyEvent(e, 'keyup.dl'); } - function keydown(e) { + this.keydown = function keydown(e) { keyEvent(e, 'keydown.dl'); } @@ -416,15 +472,38 @@ Object.assign(HookInput.prototype, { } this.events = this.events || {}; - this.events.mousedown = mousedown; - this.events.input = input; - this.events.keyup = keyup; - this.events.keydown = keydown; - this.trigger.addEventListener('mousedown', mousedown); - this.trigger.addEventListener('input', input); - this.trigger.addEventListener('keyup', keyup); - this.trigger.addEventListener('keydown', keydown); + this.events.mousedown = this.mousedown; + this.events.input = this.input; + this.events.keyup = this.keyup; + this.events.keydown = this.keydown; + this.trigger.addEventListener('mousedown', this.mousedown); + this.trigger.addEventListener('input', this.input); + this.trigger.addEventListener('keyup', this.keyup); + this.trigger.addEventListener('keydown', this.keydown); }, + + removeEvents: function(){ + this.trigger.removeEventListener('mousedown', this.mousedown); + this.trigger.removeEventListener('input', this.input); + this.trigger.removeEventListener('keyup', this.keyup); + this.trigger.removeEventListener('keydown', this.keydown); + }, + + restoreInitialState: function() { + this.list.list.innerHTML = this.list.initialState; + }, + + removePlugins: function() { + this.plugins.forEach(function(plugin) { + plugin.destroy(); + }); + }, + + destroy: function() { + this.restoreInitialState(); + this.removeEvents(); + this.removePlugins(); + } }); module.exports = HookInput; @@ -433,21 +512,14 @@ module.exports = HookInput; var DropLab = require('./droplab')(); var DATA_TRIGGER = require('./constants').DATA_TRIGGER; var keyboard = require('./keyboard')(); - var setup = function() { - var droplab = DropLab(); - require('./window')(function(w) { - w.addEventListener('load', function() { - var dropdownTriggers = [].slice.apply(document.querySelectorAll('['+DATA_TRIGGER+']')); - droplab.addHooks(dropdownTriggers).init(); - }); - }); - return droplab; + window.DropLab = DropLab; }; + module.exports = setup(); -},{"./constants":1,"./droplab":4,"./keyboard":9,"./window":11}],9:[function(require,module,exports){ +},{"./constants":1,"./droplab":4,"./keyboard":9}],9:[function(require,module,exports){ require('./window')(function(w){ module.exports = function(){ var currentKey; @@ -557,7 +629,7 @@ var camelize = function(str) { }; var closest = function(thisTag, stopTag) { - while(thisTag !== null && thisTag.tagName !== stopTag && thisTag.tagName !== 'HTML'){ + while(thisTag.tagName !== stopTag && thisTag.tagName !== 'HTML'){ thisTag = thisTag.parentNode; } return thisTag; diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js index 2dff5b83fae..b81663c281d 100644 --- a/app/assets/javascripts/droplab/droplab_ajax.js +++ b/app/assets/javascripts/droplab/droplab_ajax.js @@ -1,52 +1,59 @@ /* eslint-disable */ (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.ajax||(g.ajax = {}));g=(g.datasource||(g.datasource = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o { class DropdownAssignee extends gl.FilteredSearchDropdown { - constructor(dropdown, input) { - super(dropdown, input); + constructor(droplab, dropdown, input) { + super(droplab, dropdown, input); this.listId = 'js-dropdown-assignee'; } @@ -20,8 +20,13 @@ } renderContent() { - super.renderContent(); - droplab.setData(this.hookId, '/autocomplete/users.json?search=&per_page=20&active=true&project_id=2&group_id=&skip_ldap=&todo_filter=&todo_state_filter=¤t_user=true&push_code_to_protected_branches=&author_id=&skip_users='); + // TODO: Pass elements instead of querySelectors + this.droplab.changeHookList(this.hookId, '#js-dropdown-assignee', [droplabAjax], { + droplabAjax: { + endpoint: '/autocomplete/users.json?search=&per_page=20&active=true&project_id=2&group_id=&skip_ldap=&todo_filter=&todo_state_filter=¤t_user=true&push_code_to_protected_branches=&author_id=&skip_users=', + method: 'setData', + } + }); } filterMethod(item, query) { diff --git a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 index 37e2e80533b..c02f1e25407 100644 --- a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 @@ -3,8 +3,8 @@ ((global) => { class DropdownAuthor extends gl.FilteredSearchDropdown { - constructor(dropdown, input) { - super(dropdown, input); + constructor(droplab, dropdown, input) { + super(droplab, dropdown, input); this.listId = 'js-dropdown-author'; } @@ -16,8 +16,13 @@ } renderContent() { - super.renderContent(); - droplab.setData(this.hookId, '/autocomplete/users.json?search=&per_page=20&active=true&project_id=2&group_id=&skip_ldap=&todo_filter=&todo_state_filter=¤t_user=true&push_code_to_protected_branches=&author_id=&skip_users='); + // TODO: Pass elements instead of querySelectors + this.droplab.changeHookList(this.hookId, '#js-dropdown-author', [droplabAjax], { + droplabAjax: { + endpoint: '/autocomplete/users.json?search=&per_page=20&active=true&project_id=2&group_id=&skip_ldap=&todo_filter=&todo_state_filter=¤t_user=true&push_code_to_protected_branches=&author_id=&skip_users=', + method: 'setData', + } + }); } filterMethod(item, query) { diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index 0bee2eb2986..481faa7fd49 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -21,10 +21,9 @@ }]; class DropdownHint extends gl.FilteredSearchDropdown { - constructor(dropdown, input, filterKeyword) { - super(dropdown, input); + constructor(droplab, dropdown, input) { + super(droplab, dropdown, input); this.listId = 'js-dropdown-hint'; - this.filterKeyword = filterKeyword; } itemClicked(e) { @@ -39,8 +38,13 @@ } renderContent() { - super.renderContent(); - droplab.setData(this.hookId, dropdownData); + this.droplab.changeHookList(this.hookId, '#js-dropdown-hint', [droplabFilter], { + droplabFilter: { + template: 'hint', + filterFunction: this.filterMethod, + } + }); + this.droplab.setData(this.hookId, dropdownData); } filterMethod(item, query) { @@ -54,6 +58,14 @@ return item; } + + configure() { + this.droplab.addHook(this.input, this.dropdown, [droplabFilter], { + droplabFilter: { + template: 'hint', + } + }).init(); + } } global.DropdownHint = DropdownHint; diff --git a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 index e2c1305597a..af47ad2a1f8 100644 --- a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 @@ -3,9 +3,10 @@ ((global) => { class DropdownLabel extends gl.FilteredSearchDropdown { - constructor(dropdown, input) { - super(dropdown, input); + constructor(droplab, dropdown, input) { + super(droplab, dropdown, input); this.listId = 'js-dropdown-label'; + this.filterSymbol = '~'; } itemClicked(e) { @@ -21,20 +22,17 @@ } renderContent() { - super.renderContent(); - droplab.setData(this.hookId, 'labels.json'); - } - - filterMethod(item, query) { - const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); - const valueWithoutColon = value.slice(1).toLowerCase(); - const valueWithoutPrefix = valueWithoutColon.slice(1); - - const title = item.title.toLowerCase(); - const noTitleMatch = title.indexOf(valueWithoutPrefix) === -1 && title.indexOf(valueWithoutColon) === -1; - - item.droplab_hidden = noTitleMatch; - return item; + // TODO: Pass elements instead of querySelectors + // TODO: Don't bind filterWithSymbol to (this), just pass the symbol + this.droplab.changeHookList(this.hookId, '#js-dropdown-label', [droplabAjax, droplabFilter], { + droplabAjax: { + endpoint: 'labels.json', + method: 'setData', + }, + droplabFilter: { + filterFunction: this.filterWithSymbol.bind(this), + } + }); } } diff --git a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 index cd185d31917..9810767eb66 100644 --- a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 @@ -3,9 +3,10 @@ ((global) => { class DropdownMilestone extends gl.FilteredSearchDropdown { - constructor(dropdown, input) { - super(dropdown, input); + constructor(droplab, dropdown, input) { + super(droplab, dropdown, input); this.listId = 'js-dropdown-milestone'; + this.filterSymbol = '%'; } itemClicked(e) { @@ -21,21 +22,16 @@ } renderContent() { - super.renderContent(); - droplab.setData(this.hookId, 'milestones.json'); - } - - filterMethod(item, query) { - const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); - const valueWithoutColon = value.slice(1).toLowerCase(); - const valueWithoutPrefix = valueWithoutColon.slice(1); - - const title = item.title.toLowerCase(); - - const noTitleMatch = title.indexOf(valueWithoutPrefix) === -1 && title.indexOf(valueWithoutColon) === -1; - - item.droplab_hidden = noTitleMatch; - return item; + // TODO: Pass elements instead of querySelectors + this.droplab.changeHookList(this.hookId, '#js-dropdown-milestone', [droplabAjax, droplabFilter], { + droplabAjax: { + endpoint: 'milestones.json', + method: 'setData', + }, + droplabFilter: { + filterFunction: this.filterWithSymbol.bind(this), + } + }); } } diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 index edffd7fb8e2..80c3407b7fa 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -3,7 +3,8 @@ const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger'; class FilteredSearchDropdown { - constructor(dropdown, input) { + constructor(droplab, dropdown, input) { + this.droplab = droplab; this.hookId = 'filtered-search'; this.input = input; this.dropdown = dropdown; @@ -66,25 +67,11 @@ destroy() { this.input.setAttribute(DATA_DROPDOWN_TRIGGER, ''); - droplab.setConfig(this.getFilterConfig()); - droplab.setData(this.hookId, []); + this.droplab.setConfig(this.getFilterConfig()); + this.droplab.setData(this.hookId, []); this.unbindEvents(); } - show() { - const currentHook = this.getCurrentHook(); - if (currentHook) { - currentHook.list.show(); - } - } - - hide() { - const currentHook = this.getCurrentHook(); - if (currentHook) { - currentHook.list.hide(); - } - } - dismissDropdown() { this.input.focus(); // Propogate input change to FilteredSearchManager @@ -111,30 +98,24 @@ } getCurrentHook() { - return droplab.hooks.filter(h => h.id === this.hookId)[0]; + return this.droplab.hooks.filter(h => h.id === this.hookId)[0]; } renderContent() { - droplab.setConfig(this.getFilterConfig(this.filterKeyword)); + // Overriden by dropdown sub class } - render(hide) { + render(forceRenderContent) { this.setAsDropdown(); const firstTimeInitialized = this.getCurrentHook() === undefined; - if (firstTimeInitialized) { + if (firstTimeInitialized || forceRenderContent) { this.renderContent(); } else if(this.getCurrentHook().list.list.id !== this.listId) { - droplab.changeHookList(this.hookId, `#${this.listId}`); + // this.droplab.changeHookList(this.hookId, `#${this.listId}`); this.renderContent(); } - - if (hide) { - this.hide(); - } else { - this.show(); - } } resetFilters() { @@ -152,6 +133,29 @@ } } } + + hide() { + const currentHook = this.getCurrentHook(); + if (currentHook) { + currentHook.list.hide(); + } + } + + filterWithSymbol(item, query) { + const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); + const valueWithoutColon = value.slice(1).toLowerCase(); + const prefix = valueWithoutColon[0]; + const valueWithoutPrefix = valueWithoutColon.slice(1); + + const title = item.title.toLowerCase(); + + // Eg. this.filterSymbol = ~ for labels + const matchWithoutPrefix = prefix === this.filterSymbol && title.indexOf(valueWithoutPrefix) !== -1; + const match = title.indexOf(valueWithoutColon) !== -1; + + item.droplab_hidden = !match && !matchWithoutPrefix; + return item; + } } global.FilteredSearchDropdown = FilteredSearchDropdown; diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 841738ff627..af8e145fa7f 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -101,11 +101,18 @@ } loadDropdown(dropdownName = '', hideDropdown) { + let firstLoad = false; + const filteredSearch = document.querySelector('.filtered-search'); + + if(!this.droplab) { + firstLoad = true; + this.droplab = new DropLab(); + } + dropdownName = dropdownName.toLowerCase(); const filterIconPadding = 27; const match = gl.FilteredSearchTokenKeys.get().filter(value => value.key === dropdownName)[0]; - const filteredSearch = document.querySelector('.filtered-search'); if (!this.font) { this.font = window.getComputedStyle(filteredSearch).font; @@ -116,34 +123,38 @@ const dynamicDropdownPadding = 12; const dropdownOffset = gl.text.getTextWidth(filteredSearch.value, this.font) + filterIconPadding + dynamicDropdownPadding; + const dropdownAuthorElement = document.querySelector('#js-dropdown-author'); + const dropdownAssigneeElement = document.querySelector('#js-dropdown-assignee'); + const dropdownMilestoneElement = document.querySelector('#js-dropdown-milestone'); + const dropdownLabelElemenet = document.querySelector('#js-dropdown-label'); this.dismissCurrentDropdown(); this.currentDropdown = match.key; if (match.key === 'author') { if (!dropdownAuthor) { - dropdownAuthor = new gl.DropdownAuthor(document.querySelector('#js-dropdown-author'), filteredSearch); + dropdownAuthor = new gl.DropdownAuthor(this.droplab, dropdownAuthorElement, filteredSearch); } dropdownAuthor.setOffset(dropdownOffset); dropdownAuthor.render(); } else if (match.key === 'assignee') { if (!dropdownAssignee) { - dropdownAssignee = new gl.DropdownAssignee(document.querySelector('#js-dropdown-assignee'), filteredSearch); + dropdownAssignee = new gl.DropdownAssignee(this.droplab, dropdownAssigneeElement, filteredSearch); } dropdownAssignee.setOffset(dropdownOffset); dropdownAssignee.render(); } else if (match.key === 'milestone') { if (!dropdownMilestone) { - dropdownMilestone = new gl.DropdownMilestone(document.querySelector('#js-dropdown-milestone'), filteredSearch); + dropdownMilestone = new gl.DropdownMilestone(this.droplab, dropdownMilestoneElement, filteredSearch); } dropdownMilestone.setOffset(dropdownOffset); dropdownMilestone.render(); } else if (match.key === 'label') { if (!dropdownLabel) { - dropdownLabel = new gl.DropdownLabel(document.querySelector('#js-dropdown-label'), filteredSearch); + dropdownLabel = new gl.DropdownLabel(this.droplab, dropdownLabelElemenet, filteredSearch); } dropdownLabel.setOffset(dropdownOffset); @@ -154,22 +165,29 @@ console.log('🦄 load hint dropdown'); const dropdownOffset = gl.text.getTextWidth(filteredSearch.value, this.font) + filterIconPadding; - console.log(dropdownOffset) + const dropdownHintElement = document.querySelector('#js-dropdown-hint'); + this.dismissCurrentDropdown(); this.currentDropdown = 'hint'; - if (!dropdownHint) { - dropdownHint = new gl.DropdownHint(document.querySelector('#js-dropdown-hint'), filteredSearch, this.currentDropdown); + dropdownHint = new gl.DropdownHint(this.droplab, dropdownHintElement, filteredSearch); + } + + if (firstLoad) { + dropdownHint.configure(); } + dropdownHint.setOffset(dropdownOffset); - dropdownHint.render(hideDropdown); + dropdownHint.render(firstLoad); } } dismissCurrentDropdown() { - if (this.currentDropdown === 'hint') { - dropdownHint.destroy(); - } + // if (this.currentDropdown === 'hint') { + // dropdownHint.hide(); + // } else if (this.currentDropdown === 'author') { + // // dropdownAuthor.hide(); + // } } setDropdown() { -- cgit v1.2.1 From 3cf0ee6c1f13d111da933cdea98b46582c7c4306 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 8 Dec 2016 15:42:58 -0600 Subject: Fix turbolinks issue by cleaning up droplab on page:change --- .../filtered_search/filtered_search_manager.js.es6 | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index af8e145fa7f..f28ce6b4366 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -81,6 +81,25 @@ this.bindEvents(); loadSearchParamsFromURL(); this.setDropdown(); + + document.addEventListener('page:change', this.cleanup); + } + + cleanup() { + console.log('cleanup') + + if (this.droplab) { + this.droplab.destroy(); + this.droplab = null; + } + + dropdownHint = null; + dropdownAuthor = null; + dropdownAssignee = null; + dropdownMilestone = null; + dropdownLabel = null; + + document.removeEventListener('page:change', this.cleanup); } static addWordToInput(word, addSpace) { -- cgit v1.2.1 From c5029c65450522359ec8b869c1433ed89db3c303 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 8 Dec 2016 16:40:46 -0600 Subject: Add basic ajax filtering for author --- .../javascripts/droplab/droplab_ajax_filter.js | 109 +++++++++++++++++++++ .../filtered_search/dropdown_author.js.es6 | 34 ++++--- 2 files changed, 129 insertions(+), 14 deletions(-) create mode 100644 app/assets/javascripts/droplab/droplab_ajax_filter.js diff --git a/app/assets/javascripts/droplab/droplab_ajax_filter.js b/app/assets/javascripts/droplab/droplab_ajax_filter.js new file mode 100644 index 00000000000..b346f22f1c2 --- /dev/null +++ b/app/assets/javascripts/droplab/droplab_ajax_filter.js @@ -0,0 +1,109 @@ +/* eslint-disable */ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.ajax||(g.ajax = {}));g=(g.datasource||(g.datasource = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o -1; + if (invalidKeyPressed || this.loading) { + return; + } + + if (this.timeout) { + clearTimeout(this.timeout); + } + + this.timeout = setTimeout(this.trigger.bind(this), 200); + }, + + trigger: function trigger() { + var config = this.hook.config.droplabAjaxFilter; + var searchValue = this.trigger.value; + + if (!config || !config.endpoint || !config.searchKey) { + return; + } + + if (config.searchValueFunction) { + searchValue = config.searchValueFunction(); + } + + if (searchValue === config.searchKey) { + return this.list.show(); + } + + this.loading = true; + this.hook.list.setData([]); + + var params = config.params || {}; + params[config.searchKey] = searchValue; + var self = this; + this._loadUrlData(config.endpoint + this.buildParams(params)).then(function(data) { + self.hook.list.addData.call(self.hook.list, data[0]); + self.notLoading(); + }); + }, + + _loadUrlData: function _loadUrlData(url) { + return new Promise(function(resolve, reject) { + var xhr = new XMLHttpRequest; + xhr.open('GET', url, true); + xhr.onreadystatechange = function () { + if(xhr.readyState === XMLHttpRequest.DONE) { + if (xhr.status === 200) { + var data = JSON.parse(xhr.responseText); + return resolve([data, xhr]); + } else { + return reject([xhr.responseText, xhr.status]); + } + } + }; + xhr.send(); + }); + }, + + buildParams: function(params) { + if (!params) return ''; + var paramsArray = Object.keys(params).map(function(param) { + return param + '=' + (params[param] || ''); + }); + return '?' + paramsArray.join('&'); + }, + + destroy: function destroy() { + if (this.timeout) { + clearTimeout(this.timeout); + } + + this.hook.trigger.removeEventListener('keydown.dl', this.debounceTrigger); + this.hook.trigger.removeEventListener('focus', this.debounceTriggerWrapper); + } + }; +}); +},{"../window":2}],2:[function(require,module,exports){ +module.exports = function(callback) { + return (function() { + callback(this); + }).call(null); +}; + +},{}]},{},[1])(1) +}); \ No newline at end of file diff --git a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 index c02f1e25407..cb3a6b6ab6d 100644 --- a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 @@ -17,27 +17,33 @@ renderContent() { // TODO: Pass elements instead of querySelectors - this.droplab.changeHookList(this.hookId, '#js-dropdown-author', [droplabAjax], { - droplabAjax: { - endpoint: '/autocomplete/users.json?search=&per_page=20&active=true&project_id=2&group_id=&skip_ldap=&todo_filter=&todo_state_filter=¤t_user=true&push_code_to_protected_branches=&author_id=&skip_users=', - method: 'setData', + this.droplab.changeHookList(this.hookId, '#js-dropdown-author', [droplabAjaxFilter], { + droplabAjaxFilter: { + endpoint: '/autocomplete/users.json', + searchKey: 'search', + params: { + per_page: 20, + active: true, + project_id: 2, + current_user: true, + }, + searchValueFunction: this.getSearchInput, } }); } - filterMethod(item, query) { + getSearchInput() { + const query = document.querySelector('.filtered-search').value; const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); - const valueWithoutColon = value.slice(1).toLowerCase(); + const valueWithoutColon = value.slice(1); + const hasPrefix = valueWithoutColon[0] === '@'; const valueWithoutPrefix = valueWithoutColon.slice(1); - const username = item.username.toLowerCase(); - const name = item.name.toLowerCase(); - - const noUsernameMatch = username.indexOf(valueWithoutPrefix) === -1 && username.indexOf(valueWithoutColon) === -1; - const noNameMatch = name.indexOf(valueWithoutColon) === -1; - - item.droplab_hidden = noUsernameMatch && noNameMatch; - return item; + if (hasPrefix) { + return valueWithoutPrefix; + } else { + return valueWithoutColon; + } } } -- cgit v1.2.1 From ed4e525a3bd05dfb32aa3c2baa9bf19688319b1a Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 9 Dec 2016 11:44:09 -0600 Subject: Code cleanup --- app/assets/javascripts/droplab/droplab.js | 11 +- .../javascripts/droplab/droplab_ajax_filter.js | 13 +- .../filtered_search/dropdown_assignee.js.es6 | 41 +++-- .../filtered_search/dropdown_author.js.es6 | 31 ++-- .../filtered_search/dropdown_hint.js.es6 | 30 ++-- .../filtered_search/dropdown_label.js.es6 | 26 +-- .../filtered_search/dropdown_milestone.js.es6 | 25 +-- .../filtered_search_dropdown.js.es6 | 65 ++----- .../filtered_search/filtered_search_manager.js.es6 | 194 +++++++++------------ app/views/shared/issuable/_search_bar.html.haml | 2 +- 10 files changed, 203 insertions(+), 235 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js index 4d83b609a73..b17f156acb4 100644 --- a/app/assets/javascripts/droplab/droplab.js +++ b/app/assets/javascripts/droplab/droplab.js @@ -39,6 +39,10 @@ var DropDown = function(list, trigger) { this.getItems(); this.addEvents(); this.initialState = list.innerHTML; + + if (this.initialState.indexOf('{{') == -1) { + debugger + } }; Object.assign(DropDown.prototype, { @@ -138,6 +142,10 @@ Object.assign(DropDown.prototype, { this.list.style.display = 'none'; this.hidden = true; }, + + destroy: function() { + this.hide(); + } }); module.exports = DropDown; @@ -247,7 +255,7 @@ require('./window')(function(w){ changeHookList: function(trigger, list, plugins, config) { trigger = document.querySelector('[data-id="'+trigger+'"]'); - list = document.querySelector(list); + // list = document.querySelector(list); this.hooks.every(function(hook, i) { if(hook.trigger === trigger) { // Restore initial State @@ -503,6 +511,7 @@ Object.assign(HookInput.prototype, { this.restoreInitialState(); this.removeEvents(); this.removePlugins(); + this.list.destroy(); } }); diff --git a/app/assets/javascripts/droplab/droplab_ajax_filter.js b/app/assets/javascripts/droplab/droplab_ajax_filter.js index b346f22f1c2..c345fda1075 100644 --- a/app/assets/javascripts/droplab/droplab_ajax_filter.js +++ b/app/assets/javascripts/droplab/droplab_ajax_filter.js @@ -8,14 +8,12 @@ require('../window')(function(w){ this.hook = hook; this.notLoading(); - this.hook.trigger.addEventListener('keydown.dl', this.debounceTrigger.bind(this)); + this.debounceTriggerWrapper = this.debounceTrigger.bind(this); + this.hook.trigger.addEventListener('keydown.dl', this.debounceTriggerWrapper); + this.hook.trigger.addEventListener('focus', this.debounceTriggerWrapper); this.trigger(); }, - debounceTriggerWrapper() { - return this.debounceTrigger.bind(this.hook); - }, - notLoading: function notLoading() { this.loading = false; }, @@ -57,7 +55,8 @@ require('../window')(function(w){ params[config.searchKey] = searchValue; var self = this; this._loadUrlData(config.endpoint + this.buildParams(params)).then(function(data) { - self.hook.list.addData.call(self.hook.list, data[0]); + self.hook.restoreInitialState.call(self.hook); + self.hook.list.setData.call(self.hook.list, data[0]); self.notLoading(); }); }, @@ -93,7 +92,7 @@ require('../window')(function(w){ clearTimeout(this.timeout); } - this.hook.trigger.removeEventListener('keydown.dl', this.debounceTrigger); + this.hook.trigger.removeEventListener('keydown.dl', this.debounceTriggerWrapper); this.hook.trigger.removeEventListener('focus', this.debounceTriggerWrapper); } }; diff --git a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 index 7609546a3a6..b2b03b637e7 100644 --- a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 @@ -6,6 +6,19 @@ constructor(droplab, dropdown, input) { super(droplab, dropdown, input); this.listId = 'js-dropdown-assignee'; + this.config = { + droplabAjaxFilter: { + endpoint: '/autocomplete/users.json', + searchKey: 'search', + params: { + per_page: 20, + active: true, + project_id: 2, + current_user: true, + }, + searchValueFunction: this.getSearchInput, + } + }; } itemClicked(e) { @@ -21,27 +34,25 @@ renderContent() { // TODO: Pass elements instead of querySelectors - this.droplab.changeHookList(this.hookId, '#js-dropdown-assignee', [droplabAjax], { - droplabAjax: { - endpoint: '/autocomplete/users.json?search=&per_page=20&active=true&project_id=2&group_id=&skip_ldap=&todo_filter=&todo_state_filter=¤t_user=true&push_code_to_protected_branches=&author_id=&skip_users=', - method: 'setData', - } - }); + this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjaxFilter], this.config); } - filterMethod(item, query) { + getSearchInput() { + const query = document.querySelector('.filtered-search').value; const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); - const valueWithoutColon = value.slice(1).toLowerCase(); + const valueWithoutColon = value.slice(1); + const hasPrefix = valueWithoutColon[0] === '@'; const valueWithoutPrefix = valueWithoutColon.slice(1); - const username = item.username.toLowerCase(); - const name = item.name.toLowerCase(); - - const noUsernameMatch = username.indexOf(valueWithoutPrefix) === -1 && username.indexOf(valueWithoutColon) === -1; - const noNameMatch = name.indexOf(valueWithoutColon) === -1; + if (hasPrefix) { + return valueWithoutPrefix; + } else { + return valueWithoutColon; + } + } - item.droplab_hidden = noUsernameMatch && noNameMatch; - return item; + configure() { + this.droplab.addHook(this.input, this.dropdown, [droplabAjaxFilter], this.config).init(); } } diff --git a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 index cb3a6b6ab6d..9bd49ab1a78 100644 --- a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 @@ -6,18 +6,7 @@ constructor(droplab, dropdown, input) { super(droplab, dropdown, input); this.listId = 'js-dropdown-author'; - } - - itemClicked(e) { - const username = e.detail.selected.querySelector('.dropdown-light-content').innerText.trim(); - gl.FilteredSearchManager.addWordToInput(this.getSelectedText(username)); - - this.dismissDropdown(); - } - - renderContent() { - // TODO: Pass elements instead of querySelectors - this.droplab.changeHookList(this.hookId, '#js-dropdown-author', [droplabAjaxFilter], { + this.config = { droplabAjaxFilter: { endpoint: '/autocomplete/users.json', searchKey: 'search', @@ -29,7 +18,19 @@ }, searchValueFunction: this.getSearchInput, } - }); + }; + } + + itemClicked(e) { + const username = e.detail.selected.querySelector('.dropdown-light-content').innerText.trim(); + gl.FilteredSearchManager.addWordToInput(this.getSelectedText(username)); + + this.dismissDropdown(); + } + + renderContent() { + // TODO: Pass elements instead of querySelectors + this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjaxFilter], this.config); } getSearchInput() { @@ -45,6 +46,10 @@ return valueWithoutColon; } } + + configure() { + this.droplab.addHook(this.input, this.dropdown, [droplabAjaxFilter], this.config).init(); + } } global.DropdownAuthor = DropdownAuthor; diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index 481faa7fd49..f885267880a 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -24,26 +24,30 @@ constructor(droplab, dropdown, input) { super(droplab, dropdown, input); this.listId = 'js-dropdown-hint'; + this.config = { + droplabFilter: { + template: 'hint', + filterFunction: this.filterMethod, + } + }; } itemClicked(e) { - const token = e.detail.selected.querySelector('.js-filter-hint').innerText.trim(); - const tag = e.detail.selected.querySelector('.js-filter-tag').innerText.trim(); + const selected = e.detail.selected; + if (!selected.hasAttribute('data-value')) { + const token = selected.querySelector('.js-filter-hint').innerText.trim(); + const tag = selected.querySelector('.js-filter-tag').innerText.trim(); - if (tag.length) { - gl.FilteredSearchManager.addWordToInput(this.getSelectedText(token)); + if (tag.length) { + gl.FilteredSearchManager.addWordToInput(this.getSelectedText(token)); + } } this.dismissDropdown(); } renderContent() { - this.droplab.changeHookList(this.hookId, '#js-dropdown-hint', [droplabFilter], { - droplabFilter: { - template: 'hint', - filterFunction: this.filterMethod, - } - }); + this.droplab.changeHookList(this.hookId, this.dropdown, [droplabFilter], this.config); this.droplab.setData(this.hookId, dropdownData); } @@ -60,11 +64,7 @@ } configure() { - this.droplab.addHook(this.input, this.dropdown, [droplabFilter], { - droplabFilter: { - template: 'hint', - } - }).init(); + this.droplab.addHook(this.input, this.dropdown, [droplabFilter], this.config).init(); } } diff --git a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 index af47ad2a1f8..24a795808ca 100644 --- a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 @@ -6,7 +6,15 @@ constructor(droplab, dropdown, input) { super(droplab, dropdown, input); this.listId = 'js-dropdown-label'; - this.filterSymbol = '~'; + this.config = { + droplabAjax: { + endpoint: 'labels.json', + method: 'setData', + }, + droplabFilter: { + filterFunction: this.filterWithSymbol.bind(this, '~'), + } + }; } itemClicked(e) { @@ -22,17 +30,11 @@ } renderContent() { - // TODO: Pass elements instead of querySelectors - // TODO: Don't bind filterWithSymbol to (this), just pass the symbol - this.droplab.changeHookList(this.hookId, '#js-dropdown-label', [droplabAjax, droplabFilter], { - droplabAjax: { - endpoint: 'labels.json', - method: 'setData', - }, - droplabFilter: { - filterFunction: this.filterWithSymbol.bind(this), - } - }); + this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjax, droplabFilter], this.config); + } + + configure() { + this.droplab.addHook(this.input, this.dropdown, [droplabAjax, droplabFilter], this.config).init(); } } diff --git a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 index 9810767eb66..458a9b1c5c1 100644 --- a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 @@ -6,7 +6,15 @@ constructor(droplab, dropdown, input) { super(droplab, dropdown, input); this.listId = 'js-dropdown-milestone'; - this.filterSymbol = '%'; + this.config = { + droplabAjax: { + endpoint: 'milestones.json', + method: 'setData', + }, + droplabFilter: { + filterFunction: this.filterWithSymbol.bind(this, '%'), + } + }; } itemClicked(e) { @@ -22,16 +30,11 @@ } renderContent() { - // TODO: Pass elements instead of querySelectors - this.droplab.changeHookList(this.hookId, '#js-dropdown-milestone', [droplabAjax, droplabFilter], { - droplabAjax: { - endpoint: 'milestones.json', - method: 'setData', - }, - droplabFilter: { - filterFunction: this.filterWithSymbol.bind(this), - } - }); + this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjax, droplabFilter], this.config); + } + + configure() { + this.droplab.addHook(this.input, this.dropdown, [droplabAjax, droplabFilter], this.config).init(); } } diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 index 80c3407b7fa..2f92c7b2e2a 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -12,11 +12,16 @@ } bindEvents() { - this.dropdown.addEventListener('click.dl', this.itemClicked.bind(this)); + this.itemClickedWrapper = this.itemClicked.bind(this); + this.dropdown.addEventListener('click.dl', this.itemClickedWrapper); } unbindEvents() { - this.dropdown.removeEventListener('click.dl', this.itemClicked.bind(this)); + this.dropdown.removeEventListener('click.dl', this.itemClickedWrapper); + } + + getCurrentHook() { + return this.droplab.hooks.filter(h => h.id === this.hookId)[0]; } getEscapedText(text) { @@ -49,34 +54,8 @@ // Overridden by dropdown sub class } - getFilterConfig(filterKeyword) { - const config = {}; - const filterConfig = {}; - - if (filterKeyword) { - filterConfig.text = filterKeyword; - } - - if (this.filterMethod) { - filterConfig.filter = this.filterMethod; - } - - config[this.hookId] = filterConfig; - return config; - } - - destroy() { - this.input.setAttribute(DATA_DROPDOWN_TRIGGER, ''); - this.droplab.setConfig(this.getFilterConfig()); - this.droplab.setData(this.hookId, []); - this.unbindEvents(); - } - - dismissDropdown() { - this.input.focus(); - // Propogate input change to FilteredSearchManager - // so that it can determine which dropdowns to open - this.input.dispatchEvent(new Event('input')); + renderContent() { + // Overriden by dropdown sub class } setAsDropdown() { @@ -97,13 +76,12 @@ return dataValue !== null; } - getCurrentHook() { - return this.droplab.hooks.filter(h => h.id === this.hookId)[0]; - } - - renderContent() { - // Overriden by dropdown sub class - } + dismissDropdown() { + this.input.focus(); + // Propogate input change to FilteredSearchManager + // so that it can determine which dropdowns to open + this.input.dispatchEvent(new Event('input')); + } render(forceRenderContent) { this.setAsDropdown(); @@ -134,14 +112,7 @@ } } - hide() { - const currentHook = this.getCurrentHook(); - if (currentHook) { - currentHook.list.hide(); - } - } - - filterWithSymbol(item, query) { + filterWithSymbol(filterSymbol, item, query) { const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); const valueWithoutColon = value.slice(1).toLowerCase(); const prefix = valueWithoutColon[0]; @@ -149,8 +120,8 @@ const title = item.title.toLowerCase(); - // Eg. this.filterSymbol = ~ for labels - const matchWithoutPrefix = prefix === this.filterSymbol && title.indexOf(valueWithoutPrefix) !== -1; + // Eg. filterSymbol = ~ for labels + const matchWithoutPrefix = prefix === filterSymbol && title.indexOf(valueWithoutPrefix) !== -1; const match = title.indexOf(valueWithoutColon) !== -1; item.droplab_hidden = !match && !matchWithoutPrefix; diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index f28ce6b4366..9846f3ba50d 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -69,20 +69,19 @@ } } - let dropdownHint; - let dropdownAuthor; - let dropdownAssignee; - let dropdownMilestone; - let dropdownLabel; - class FilteredSearchManager { constructor() { this.tokenizer = gl.FilteredSearchTokenizer; + this.filteredSearchInput = document.querySelector('.filtered-search'); + this.clearSearchButton = document.querySelector('.clear-search'); + + this.setupMapping(); this.bindEvents(); loadSearchParamsFromURL(); this.setDropdown(); - document.addEventListener('page:change', this.cleanup); + this.cleanupWrapper = this.cleanup.bind(this); + document.addEventListener('page:fetch', this.cleanupWrapper); } cleanup() { @@ -93,124 +92,105 @@ this.droplab = null; } - dropdownHint = null; - dropdownAuthor = null; - dropdownAssignee = null; - dropdownMilestone = null; - dropdownLabel = null; + this.setupMapping(); + + document.removeEventListener('page:fetch', this.cleanupWrapper); + } - document.removeEventListener('page:change', this.cleanup); + setupMapping() { + this.mapping = { + author: { + reference: null, + gl: 'DropdownAuthor', + element: document.querySelector('#js-dropdown-author'), + }, + assignee: { + reference: null, + gl: 'DropdownAssignee', + element: document.querySelector('#js-dropdown-assignee'), + }, + milestone: { + reference: null, + gl: 'DropdownMilestone', + element: document.querySelector('#js-dropdown-milestone'), + }, + label: { + reference: null, + gl: 'DropdownLabel', + element: document.querySelector('#js-dropdown-label'), + }, + hint: { + reference: null, + gl: 'DropdownHint', + element: document.querySelector('#js-dropdown-hint'), + }, + } } static addWordToInput(word, addSpace) { - const filteredSearchValue = document.querySelector('.filtered-search').value; + const filteredSearchInput = document.querySelector('.filtered-search') + const filteredSearchValue = filteredSearchInput.value; const hasExistingValue = filteredSearchValue.length !== 0; - const { lastToken } = gl.FilteredSearchTokenizer.processTokens(filteredSearchValue); + if (lastToken.hasOwnProperty('key')) { console.log(lastToken); // Spaces inside the token means that the token value will be escaped by quotes const hasQuotes = lastToken.value.indexOf(' ') !== -1; - const lengthToRemove = hasQuotes ? lastToken.value.length + 2 : lastToken.value.length; - document.querySelector('.filtered-search').value = filteredSearchValue.slice(0, -1 * (lengthToRemove)); + filteredSearchInput.value = filteredSearchValue.slice(0, -1 * (lengthToRemove)); } - document.querySelector('.filtered-search').value += hasExistingValue && addSpace ? ` ${word}` : word; + filteredSearchInput.value += hasExistingValue && addSpace ? ` ${word}` : word; } - loadDropdown(dropdownName = '', hideDropdown) { - let firstLoad = false; - const filteredSearch = document.querySelector('.filtered-search'); - - if(!this.droplab) { - firstLoad = true; - this.droplab = new DropLab(); - } - - dropdownName = dropdownName.toLowerCase(); - + load(key, firstLoad = false) { + console.log(`🦄 load ${key} dropdown`); + const glClass = this.mapping[key].gl; + const element = this.mapping[key].element; const filterIconPadding = 27; - const match = gl.FilteredSearchTokenKeys.get().filter(value => value.key === dropdownName)[0]; + const dropdownOffset = gl.text.getTextWidth(this.filteredSearchInput.value, this.font) + filterIconPadding; - if (!this.font) { - this.font = window.getComputedStyle(filteredSearch).font; + if (!this.mapping[key].reference) { + this.mapping[key].reference = new gl[glClass](this.droplab, element, this.filteredSearchInput); } - if (match && this.currentDropdown !== match.key) { - console.log(`🦄 load ${match.key} dropdown`); - - const dynamicDropdownPadding = 12; - const dropdownOffset = gl.text.getTextWidth(filteredSearch.value, this.font) + filterIconPadding + dynamicDropdownPadding; - const dropdownAuthorElement = document.querySelector('#js-dropdown-author'); - const dropdownAssigneeElement = document.querySelector('#js-dropdown-assignee'); - const dropdownMilestoneElement = document.querySelector('#js-dropdown-milestone'); - const dropdownLabelElemenet = document.querySelector('#js-dropdown-label'); - - this.dismissCurrentDropdown(); - this.currentDropdown = match.key; - - if (match.key === 'author') { - if (!dropdownAuthor) { - dropdownAuthor = new gl.DropdownAuthor(this.droplab, dropdownAuthorElement, filteredSearch); - } - - dropdownAuthor.setOffset(dropdownOffset); - dropdownAuthor.render(); - } else if (match.key === 'assignee') { - if (!dropdownAssignee) { - dropdownAssignee = new gl.DropdownAssignee(this.droplab, dropdownAssigneeElement, filteredSearch); - } - - dropdownAssignee.setOffset(dropdownOffset); - dropdownAssignee.render(); - } else if (match.key === 'milestone') { - if (!dropdownMilestone) { - dropdownMilestone = new gl.DropdownMilestone(this.droplab, dropdownMilestoneElement, filteredSearch); - } + if (firstLoad) { + this.mapping[key].reference.configure(); + } - dropdownMilestone.setOffset(dropdownOffset); - dropdownMilestone.render(); - } else if (match.key === 'label') { - if (!dropdownLabel) { - dropdownLabel = new gl.DropdownLabel(this.droplab, dropdownLabelElemenet, filteredSearch); - } + this.mapping[key].reference.setOffset(dropdownOffset); + this.mapping[key].reference.render(firstLoad); - dropdownLabel.setOffset(dropdownOffset); - dropdownLabel.render(); - } + this.currentDropdown = key; + } - } else if (!match && this.currentDropdown !== 'hint') { - console.log('🦄 load hint dropdown'); + loadDropdown(dropdownName = '') { + let firstLoad = false; - const dropdownOffset = gl.text.getTextWidth(filteredSearch.value, this.font) + filterIconPadding; - const dropdownHintElement = document.querySelector('#js-dropdown-hint'); + if(!this.droplab) { + firstLoad = true; + this.droplab = new DropLab(); + } - this.dismissCurrentDropdown(); - this.currentDropdown = 'hint'; - if (!dropdownHint) { - dropdownHint = new gl.DropdownHint(this.droplab, dropdownHintElement, filteredSearch); - } + if (!this.font) { + this.font = window.getComputedStyle(this.filteredSearchInput).font; + } - if (firstLoad) { - dropdownHint.configure(); - } + const match = gl.FilteredSearchTokenKeys.get().filter(value => value.key === dropdownName.toLowerCase())[0]; + const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key && this.mapping.hasOwnProperty(match.key); + const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint'; - dropdownHint.setOffset(dropdownOffset); - dropdownHint.render(firstLoad); + if (shouldOpenFilterDropdown || shouldOpenHintDropdown) { + const key = match && match.hasOwnProperty('key') ? match.key : 'hint'; + this.load(key, firstLoad); } - } - dismissCurrentDropdown() { - // if (this.currentDropdown === 'hint') { - // dropdownHint.hide(); - // } else if (this.currentDropdown === 'author') { - // // dropdownAuthor.hide(); - // } + gl.droplab = this.droplab; } setDropdown() { - const { lastToken } = this.tokenizer.processTokens(document.querySelector('.filtered-search').value); + const { lastToken } = this.tokenizer.processTokens(this.filteredSearchInput.value); if (typeof lastToken === 'string') { // Token is not fully initialized yet @@ -228,32 +208,20 @@ } bindEvents() { - const filteredSearchInput = document.querySelector('.filtered-search'); - - filteredSearchInput.addEventListener('input', this.setDropdown.bind(this)); - filteredSearchInput.addEventListener('input', toggleClearSearchButton); - filteredSearchInput.addEventListener('keydown', this.checkForEnter.bind(this)); - document.querySelector('.clear-search').addEventListener('click', this.clearSearch.bind(this)); + this.filteredSearchInput.addEventListener('input', this.setDropdown.bind(this)); + this.filteredSearchInput.addEventListener('input', toggleClearSearchButton); + this.filteredSearchInput.addEventListener('keydown', this.checkForEnter.bind(this)); + this.clearSearchButton.addEventListener('click', this.clearSearch.bind(this)); } clearSearch(e) { e.stopPropagation(); e.preventDefault(); - document.querySelector('.filtered-search').value = ''; - document.querySelector('.clear-search').classList.add('hidden'); + this.filteredSearchInput.value = ''; + this.clearSearchButton.classList.add('hidden'); dropdownHint.resetFilters(); - this.loadDropdown('hint', true); - } - - checkDropdownToken(e) { - const input = e.target.value; - const { lastToken } = this.tokenizer.processTokens(input); - - // Check for dropdown token - if (lastToken[lastToken.length - 1] === ':') { - const token = lastToken.slice(0, -1); - } + this.loadDropdown('hint'); } checkForEnter(e) { diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 0a5de59cb63..53983ef8d6d 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -18,7 +18,7 @@ = icon('times') #js-dropdown-hint.dropdown-menu.hint-dropdown %ul - %li.filter-dropdown-item{ 'data-value': 'none' } + %li.filter-dropdown-item{ 'data-value': '' } %button.btn.btn-link = icon('search') %span -- cgit v1.2.1 From c18285cec0dadc0af8fa062a10d6cd581efb0b66 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 9 Dec 2016 12:11:29 -0600 Subject: Fixed issue where dropdown would not open after clicking on a dropdown item --- app/assets/javascripts/droplab/droplab.js | 3 +-- app/views/shared/issuable/_search_bar.html.haml | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js index b17f156acb4..6b326338050 100644 --- a/app/assets/javascripts/droplab/droplab.js +++ b/app/assets/javascripts/droplab/droplab.js @@ -234,8 +234,7 @@ require('./window')(function(w){ var self = this; this.windowClickedWrapper = function(e){ var thisTag = e.target; - if(thisTag.tagName === 'LI' || thisTag.tagName === 'A' - || thisTag.tagName === 'BUTTON'){ + if(thisTag.tagName !== 'UL'){ // climb up the tree to find the UL thisTag = utils.closest(thisTag, 'UL'); } diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 53983ef8d6d..2d2ecf030a8 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -17,13 +17,13 @@ %button.clear-search.hidden{ type: 'button' } = icon('times') #js-dropdown-hint.dropdown-menu.hint-dropdown - %ul + %ul{ 'data-dropdown' => true } %li.filter-dropdown-item{ 'data-value': '' } %button.btn.btn-link = icon('search') %span Keep typing and press Enter - %ul.filter-dropdown{ 'data-dynamic' => true } + %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item %button.btn.btn-link %i.fa{ 'class': '{{icon}}'} @@ -31,8 +31,8 @@ {{hint}} %span.js-filter-tag.dropdown-light-content {{tag}} - #js-dropdown-author.dropdown-menu{ 'data-dropdown' => true } - %ul.filter-dropdown{ 'data-dynamic' => true } + #js-dropdown-author.dropdown-menu + %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item %button.btn.btn-link.dropdown-user %img.avatar.avatar-inline{ 'data-src': '{{avatar_url}}', width: '30' } @@ -41,13 +41,13 @@ {{name}} %span.dropdown-light-content @{{username}} - #js-dropdown-assignee.dropdown-menu{ 'data-dropdown' => true } - %ul + #js-dropdown-assignee.dropdown-menu + %ul{ 'data-dropdown' => true } %li.filter-dropdown-item{ 'data-value': 'none' } %button.btn.btn-link No assignee %li.divider - %ul.filter-dropdown{ 'data-dynamic' => true } + %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item %button.btn.btn-link.dropdown-user %img.avatar.avatar-inline{ 'data-src': '{{avatar_url}}', width: '30' } @@ -57,22 +57,22 @@ %span.dropdown-light-content @{{username}} #js-dropdown-milestone.dropdown-menu{ 'data-dropdown' => true } - %ul + %ul{ 'data-dropdown' => true } %li.filter-dropdown-item{ 'data-value': 'none' } %button.btn.btn-link No milestone %li.divider - %ul.filter-dropdown{ 'data-dynamic' => true } + %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item %button.btn.btn-link {{title}} #js-dropdown-label.dropdown-menu{ 'data-dropdown' => true } - %ul + %ul{ 'data-dropdown' => true } %li.filter-dropdown-item{ 'data-value': 'none' } %button.btn.btn-link No label %li.divider - %ul.filter-dropdown{ 'data-dynamic' => true } + %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item %button.btn.btn-link %span.dropdown-label-box{ 'style': 'background: {{color}}'} -- cgit v1.2.1 From f0608878ce8fd8dfaa91a285eff6d3db11f509c3 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 9 Dec 2016 13:15:09 -0600 Subject: Fix bug where dropdowns would not dismiss properly --- app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 | 2 +- app/assets/javascripts/filtered_search/dropdown_hint.js.es6 | 9 ++++++--- app/assets/javascripts/filtered_search/dropdown_label.js.es6 | 3 ++- app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 | 2 +- .../javascripts/filtered_search/filtered_search_dropdown.js.es6 | 7 +++++-- 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 index b2b03b637e7..850cca670e4 100644 --- a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 @@ -29,7 +29,7 @@ gl.FilteredSearchManager.addWordToInput(this.getSelectedText(username)); } - this.dismissDropdown(); + this.dismissDropdown(!dataValueSet); } renderContent() { diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index f885267880a..ea384af09a9 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -34,16 +34,19 @@ itemClicked(e) { const selected = e.detail.selected; - if (!selected.hasAttribute('data-value')) { + + if (selected.hasAttribute('data-value')) { + this.dismissDropdown(); + } else { const token = selected.querySelector('.js-filter-hint').innerText.trim(); const tag = selected.querySelector('.js-filter-tag').innerText.trim(); if (tag.length) { gl.FilteredSearchManager.addWordToInput(this.getSelectedText(token)); } + this.dismissDropdown(); + this.dispatchInputEvent(); } - - this.dismissDropdown(); } renderContent() { diff --git a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 index 24a795808ca..c79df0aee4a 100644 --- a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 @@ -26,7 +26,8 @@ gl.FilteredSearchManager.addWordToInput(labelName); } - this.dismissDropdown(); + // debugger + this.dismissDropdown(!dataValueSet); } renderContent() { diff --git a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 index 458a9b1c5c1..10535097747 100644 --- a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 @@ -26,7 +26,7 @@ gl.FilteredSearchManager.addWordToInput(this.getSelectedText(milestoneName)); } - this.dismissDropdown(); + this.dismissDropdown(!dataValueSet); } renderContent() { diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 index 2f92c7b2e2a..cd46e430e01 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -77,11 +77,15 @@ } dismissDropdown() { + this.getCurrentHook().list.hide(); this.input.focus(); + } + + dispatchInputEvent() { // Propogate input change to FilteredSearchManager // so that it can determine which dropdowns to open this.input.dispatchEvent(new Event('input')); - } + } render(forceRenderContent) { this.setAsDropdown(); @@ -91,7 +95,6 @@ if (firstTimeInitialized || forceRenderContent) { this.renderContent(); } else if(this.getCurrentHook().list.list.id !== this.listId) { - // this.droplab.changeHookList(this.hookId, `#${this.listId}`); this.renderContent(); } } -- cgit v1.2.1 From ddc42a61f064757d69d4c61f784dacbcd1334d4d Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 9 Dec 2016 13:15:38 -0600 Subject: Fix bug where token values with 2 double quotes were not treated as a complete value --- .../filtered_search/filtered_search_tokenizer.es6 | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 index d6df83a3fb9..5ad433f4a09 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 @@ -119,19 +119,26 @@ const keyMatch = validTokenKeys.filter(v => v.key === tokenKey)[0]; const symbolMatch = validTokenKeys.filter(v => v.symbol === tokenSymbol)[0]; + const doubleQuoteOccurrences = tokenValue.split('"').length - 1; + const singleQuoteOccurrences = tokenValue.split('\'').length - 1; + const doubleQuoteIndex = tokenValue.indexOf('"'); const singleQuoteIndex = tokenValue.indexOf('\''); const doubleQuoteExist = doubleQuoteIndex !== -1; const singleQuoteExist = singleQuoteIndex !== -1; - if ((doubleQuoteExist && !singleQuoteExist) || - (doubleQuoteExist && singleQuoteExist && doubleQuoteIndex < singleQuoteIndex)) { + const doubleQuoteExistOnly = doubleQuoteExist && !singleQuoteExist; + const doubleQuoteIsBeforeSingleQuote = doubleQuoteExist && singleQuoteExist && doubleQuoteIndex < singleQuoteIndex; + + const singleQuoteExistOnly = singleQuoteExist && !doubleQuoteExist; + const singleQuoteIsBeforeDoubleQuote = doubleQuoteExist && singleQuoteExist && singleQuoteIndex < doubleQuoteIndex; + + if ((doubleQuoteExistOnly || doubleQuoteIsBeforeSingleQuote) && doubleQuoteOccurrences % 2 !== 0) { // " is found and is in front of ' (if any) lastQuotation = '"'; incompleteToken = true; - } else if ((singleQuoteExist && !doubleQuoteExist) || - (doubleQuoteExist && singleQuoteExist && singleQuoteIndex < doubleQuoteIndex)) { + } else if ((singleQuoteExistOnly || singleQuoteIsBeforeDoubleQuote) && singleQuoteOccurrences % 2 !== 0) { // ' is found and is in front of " (if any) lastQuotation = '\''; incompleteToken = true; -- cgit v1.2.1 From 002d17f1b7e4d298f45cb7d0dc944e20cbd734b5 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 9 Dec 2016 13:17:19 -0600 Subject: Fix clear button --- app/assets/javascripts/droplab/droplab.js | 51 ++++++++++++---------- .../filtered_search/dropdown_assignee.js.es6 | 4 +- .../filtered_search/dropdown_author.js.es6 | 4 +- .../filtered_search/dropdown_label.js.es6 | 3 +- .../filtered_search/dropdown_milestone.js.es6 | 3 +- .../filtered_search_dropdown.js.es6 | 35 +++++---------- .../filtered_search/filtered_search_manager.js.es6 | 17 ++++++-- .../filtered_search/filtered_search_tokenizer.es6 | 10 ++--- 8 files changed, 66 insertions(+), 61 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js index 6b326338050..42ddb7a4a56 100644 --- a/app/assets/javascripts/droplab/droplab.js +++ b/app/assets/javascripts/droplab/droplab.js @@ -51,26 +51,28 @@ Object.assign(DropDown.prototype, { return this.items; }, + clickEvent: function(e) { + // climb up the tree to find the LI + var selected = utils.closest(e.target, 'LI'); + + if(selected) { + e.preventDefault(); + this.hide(); + var listEvent = new CustomEvent('click.dl', { + detail: { + list: this, + selected: selected, + data: e.target.dataset, + }, + }); + this.list.dispatchEvent(listEvent); + } + }, + addEvents: function() { - var self = this; + this.clickWrapper = this.clickEvent.bind(this); // event delegation. - this.list.addEventListener('click', function(e) { - // climb up the tree to find the LI - var selected = utils.closest(e.target, 'LI'); - - if(selected) { - e.preventDefault(); - self.hide(); - var listEvent = new CustomEvent('click.dl', { - detail: { - list: self, - selected: selected, - data: e.target.dataset, - }, - }); - self.list.dispatchEvent(listEvent); - } - }); + this.list.addEventListener('click', this.clickWrapper); }, toggle: function() { @@ -93,6 +95,7 @@ Object.assign(DropDown.prototype, { // call render manually on data; render: function(data){ + // debugger // empty the list first var sampleItem; var newChildren = []; @@ -134,17 +137,23 @@ Object.assign(DropDown.prototype, { }, show: function() { + // debugger this.list.style.display = 'block'; this.hidden = false; }, hide: function() { + // debugger this.list.style.display = 'none'; this.hidden = true; }, destroy: function() { - this.hide(); + if (!this.hidden) { + this.hide(); + } + + this.list.removeEventListener('click', this.clickWrapper); } }); @@ -257,10 +266,6 @@ require('./window')(function(w){ // list = document.querySelector(list); this.hooks.every(function(hook, i) { if(hook.trigger === trigger) { - // Restore initial State - hook.list.list.innerHTML = hook.list.initialState; - hook.list.hide(); - hook.destroy(); this.hooks.splice(i, 1); this.addHook(trigger, list, plugins, config); diff --git a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 index 850cca670e4..3420397edda 100644 --- a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 @@ -32,9 +32,9 @@ this.dismissDropdown(!dataValueSet); } - renderContent() { - // TODO: Pass elements instead of querySelectors + renderContent(forceShowList = false) { this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjaxFilter], this.config); + super.renderContent(forceShowList); } getSearchInput() { diff --git a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 index 9bd49ab1a78..f1401f6f9d2 100644 --- a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 @@ -28,9 +28,9 @@ this.dismissDropdown(); } - renderContent() { - // TODO: Pass elements instead of querySelectors + renderContent(forceShowList) { this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjaxFilter], this.config); + super.renderContent(forceShowList); } getSearchInput() { diff --git a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 index c79df0aee4a..4c9926c1f78 100644 --- a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 @@ -30,8 +30,9 @@ this.dismissDropdown(!dataValueSet); } - renderContent() { + renderContent(forceShowList) { this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjax, droplabFilter], this.config); + super.renderContent(forceShowList); } configure() { diff --git a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 index 10535097747..33967ddff24 100644 --- a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 @@ -29,8 +29,9 @@ this.dismissDropdown(!dataValueSet); } - renderContent() { + renderContent(forceShowList = false) { this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjax, droplabFilter], this.config); + super.renderContent(forceShowList); } configure() { diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 index cd46e430e01..163dac65842 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -4,6 +4,7 @@ class FilteredSearchDropdown { constructor(droplab, dropdown, input) { + console.log('constructor'); this.droplab = droplab; this.hookId = 'filtered-search'; this.input = input; @@ -54,8 +55,10 @@ // Overridden by dropdown sub class } - renderContent() { - // Overriden by dropdown sub class + renderContent(forceShowList = false) { + if (forceShowList && this.getCurrentHook().list.hidden) { + this.getCurrentHook().list.show(); + } } setAsDropdown() { @@ -77,7 +80,6 @@ } dismissDropdown() { - this.getCurrentHook().list.hide(); this.input.focus(); } @@ -87,31 +89,16 @@ this.input.dispatchEvent(new Event('input')); } - render(forceRenderContent) { + render(forceRenderContent = false, forceShowList = false) { this.setAsDropdown(); - const firstTimeInitialized = this.getCurrentHook() === undefined; - - if (firstTimeInitialized || forceRenderContent) { - this.renderContent(); - } else if(this.getCurrentHook().list.list.id !== this.listId) { - this.renderContent(); - } - } - - resetFilters() { const currentHook = this.getCurrentHook(); + const firstTimeInitialized = currentHook === undefined; - if (currentHook) { - const list = currentHook.list; - - if (list.data) { - const data = list.data.map((item) => { - item.droplab_hidden = false; - }); - - list.render(data); - } + if (firstTimeInitialized || forceRenderContent) { + this.renderContent(forceShowList); + } else if(currentHook.list.list.id !== this.listId) { + this.renderContent(forceShowList); } } diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 9846f3ba50d..4f5d144bff3 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -150,6 +150,7 @@ const element = this.mapping[key].element; const filterIconPadding = 27; const dropdownOffset = gl.text.getTextWidth(this.filteredSearchInput.value, this.font) + filterIconPadding; + let forceShowList = false; if (!this.mapping[key].reference) { this.mapping[key].reference = new gl[glClass](this.droplab, element, this.filteredSearchInput); @@ -159,8 +160,13 @@ this.mapping[key].reference.configure(); } + if (this.currentDropdown === 'hint') { + // Clicked from hint dropdown + forceShowList = true; + } + this.mapping[key].reference.setOffset(dropdownOffset); - this.mapping[key].reference.render(firstLoad); + this.mapping[key].reference.render(firstLoad, forceShowList); this.currentDropdown = key; } @@ -207,6 +213,12 @@ } } + // dismissCurrentDropdown() { + // if (this.currentDropdown === 'hint') { + // this.mapping['hint'].hide(); + // } + // } + bindEvents() { this.filteredSearchInput.addEventListener('input', this.setDropdown.bind(this)); this.filteredSearchInput.addEventListener('input', toggleClearSearchButton); @@ -220,8 +232,7 @@ this.filteredSearchInput.value = ''; this.clearSearchButton.classList.add('hidden'); - dropdownHint.resetFilters(); - this.loadDropdown('hint'); + this.setDropdown(); } checkForEnter(e) { diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 index 5ad433f4a09..4abb5e94d81 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 @@ -3,11 +3,11 @@ class FilteredSearchTokenizer { // TODO: Remove when going to pro static printTokens(tokens, searchToken, lastToken) { - // console.log('tokens:'); - // tokens.forEach(token => console.log(token)); - // console.log(`search: ${searchToken}`); - // console.log('last token:'); - // console.log(lastToken); + console.log('tokens:'); + tokens.forEach(token => console.log(token)); + console.log(`search: ${searchToken}`); + console.log('last token:'); + console.log(lastToken); } static parseToken(input) { -- cgit v1.2.1 From 21d1d9b2c0d794426de73aab6924dbbe05e61ba5 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 9 Dec 2016 15:10:15 -0600 Subject: Add username to page_filter_path --- app/helpers/application_helper.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index c816b616631..a112928c6de 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -244,7 +244,9 @@ module ApplicationHelper scope: params[:scope], milestone_title: params[:milestone_title], assignee_id: params[:assignee_id], + assignee_username: params[:assignee_username], author_id: params[:author_id], + author_username: params[:author_username], search: params[:search], label_name: params[:label_name] } -- cgit v1.2.1 From 0eb9f53715a8fbbe8ede0043aad5a9c9377716e5 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 9 Dec 2016 15:20:41 -0600 Subject: Prevent droplab from opening dropdown by cleaning it --- app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 4f5d144bff3..91de7783cc1 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -240,6 +240,10 @@ if (e.keyCode === 13) { e.stopPropagation(); e.preventDefault(); + + // Prevent droplab from opening dropdown + this.droplab.destroy(); + this.search(); } } -- cgit v1.2.1 From e5b061b801ee7b806dd401c97a5b268e3f34859a Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 9 Dec 2016 15:36:44 -0600 Subject: Reposition dropdown when backspace is hit --- .../filtered_search/filtered_search_manager.js.es6 | 45 ++++++++++++++++------ 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 91de7783cc1..d21ae70cdb9 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -94,6 +94,7 @@ this.setupMapping(); + this.unbindEvents(); document.removeEventListener('page:fetch', this.cleanupWrapper); } @@ -144,12 +145,17 @@ filteredSearchInput.value += hasExistingValue && addSpace ? ` ${word}` : word; } + updateDropdownOffset(key) { + const filterIconPadding = 27; + const offset = gl.text.getTextWidth(this.filteredSearchInput.value, this.font) + filterIconPadding; + + this.mapping[key].reference.setOffset(offset); + } + load(key, firstLoad = false) { console.log(`🦄 load ${key} dropdown`); const glClass = this.mapping[key].gl; const element = this.mapping[key].element; - const filterIconPadding = 27; - const dropdownOffset = gl.text.getTextWidth(this.filteredSearchInput.value, this.font) + filterIconPadding; let forceShowList = false; if (!this.mapping[key].reference) { @@ -165,7 +171,7 @@ forceShowList = true; } - this.mapping[key].reference.setOffset(dropdownOffset); + this.updateDropdownOffset(key); this.mapping[key].reference.render(firstLoad, forceShowList); this.currentDropdown = key; @@ -213,17 +219,25 @@ } } - // dismissCurrentDropdown() { - // if (this.currentDropdown === 'hint') { - // this.mapping['hint'].hide(); - // } - // } - bindEvents() { - this.filteredSearchInput.addEventListener('input', this.setDropdown.bind(this)); + this.setDropdownWrapper = this.setDropdown.bind(this); + this.checkForEnterWrapper = this.checkForEnter.bind(this); + this.clearSearchWrapper = this.clearSearch.bind(this); + this.checkForBackspaceWrapper = this.checkForBackspace.bind(this); + + this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper); this.filteredSearchInput.addEventListener('input', toggleClearSearchButton); - this.filteredSearchInput.addEventListener('keydown', this.checkForEnter.bind(this)); - this.clearSearchButton.addEventListener('click', this.clearSearch.bind(this)); + this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper); + this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper); + this.clearSearchButton.addEventListener('click', this.clearSearchWrapper); + } + + unbindEvents() { + this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper); + this.filteredSearchInput.removeEventListener('input', toggleClearSearchButton); + this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper); + this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper); + this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper); } clearSearch(e) { @@ -235,6 +249,13 @@ this.setDropdown(); } + checkForBackspace(e) { + if (e.keyCode === 8) { + // Reposition dropdown so that it is aligned with cursor + this.updateDropdownOffset(this.currentDropdown); + } + } + checkForEnter(e) { // Enter KeyCode if (e.keyCode === 13) { -- cgit v1.2.1 From 88ed015915e63c07181122c461523d3e1610e98e Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 9 Dec 2016 15:47:56 -0600 Subject: Fix clear button so that it resets the dropdowns properly --- .../javascripts/filtered_search/filtered_search_dropdown.js.es6 | 4 ++++ .../javascripts/filtered_search/filtered_search_manager.js.es6 | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 index 163dac65842..03835b6522b 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -117,6 +117,10 @@ item.droplab_hidden = !match && !matchWithoutPrefix; return item; } + + hideDropdown() { + this.getCurrentHook().list.hide(); + } } global.FilteredSearchDropdown = FilteredSearchDropdown; diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index d21ae70cdb9..0654d7d816a 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -246,7 +246,15 @@ this.filteredSearchInput.value = ''; this.clearSearchButton.classList.add('hidden'); + + // Force dropdown to hide + this.mapping[this.currentDropdown].reference.hideDropdown(); + + // Re-Load dropdown this.setDropdown(); + + // Reposition dropdown so that it is aligned with cursor + this.updateDropdownOffset(this.currentDropdown); } checkForBackspace(e) { -- cgit v1.2.1 From 98a95633f558f4f4762d4e523ef0c495d6f79f68 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 9 Dec 2016 16:02:43 -0600 Subject: Reset filters when clear search is clicked --- .../javascripts/filtered_search/filtered_search_dropdown.js.es6 | 9 +++++++++ .../javascripts/filtered_search/filtered_search_manager.js.es6 | 3 +++ 2 files changed, 12 insertions(+) diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 index 03835b6522b..5186c15cb67 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -121,6 +121,15 @@ hideDropdown() { this.getCurrentHook().list.hide(); } + + resetFilters() { + const hook = this.getCurrentHook(); + const data = hook.list.data; + const results = data.map(function(o) { + o.droplab_hidden = false; + }); + hook.list.render(results); + } } global.FilteredSearchDropdown = FilteredSearchDropdown; diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 0654d7d816a..c7e01fc710d 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -247,6 +247,9 @@ this.filteredSearchInput.value = ''; this.clearSearchButton.classList.add('hidden'); + // Reset Filters + this.mapping[this.currentDropdown].reference.resetFilters(); + // Force dropdown to hide this.mapping[this.currentDropdown].reference.hideDropdown(); -- cgit v1.2.1 From 7b202460f7333e1a9997f1ee79eca71975cc676f Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 11:13:17 -0600 Subject: Fix ajax bug --- app/assets/javascripts/droplab/droplab.js | 7 +------ app/assets/javascripts/droplab/droplab_ajax_filter.js | 9 +++++++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js index 42ddb7a4a56..359cd82bbcd 100644 --- a/app/assets/javascripts/droplab/droplab.js +++ b/app/assets/javascripts/droplab/droplab.js @@ -31,18 +31,13 @@ if ( typeof CustomEvent === "function" ) { var CustomEvent = require('./custom_event_polyfill'); var utils = require('./utils'); -var DropDown = function(list, trigger) { +var DropDown = function(list) { this.hidden = true; this.list = list; - this.trigger = trigger; this.items = []; this.getItems(); this.addEvents(); this.initialState = list.innerHTML; - - if (this.initialState.indexOf('{{') == -1) { - debugger - } }; Object.assign(DropDown.prototype, { diff --git a/app/assets/javascripts/droplab/droplab_ajax_filter.js b/app/assets/javascripts/droplab/droplab_ajax_filter.js index c345fda1075..0d6a7892bdc 100644 --- a/app/assets/javascripts/droplab/droplab_ajax_filter.js +++ b/app/assets/javascripts/droplab/droplab_ajax_filter.js @@ -5,6 +5,7 @@ require('../window')(function(w){ w.droplabAjaxFilter = { init: function(hook) { + this.destroyed = false; this.hook = hook; this.notLoading(); @@ -49,14 +50,16 @@ require('../window')(function(w){ } this.loading = true; + this.hook.list.setData([]); var params = config.params || {}; params[config.searchKey] = searchValue; var self = this; this._loadUrlData(config.endpoint + this.buildParams(params)).then(function(data) { - self.hook.restoreInitialState.call(self.hook); - self.hook.list.setData.call(self.hook.list, data[0]); + if (!self.destroyed) { + self.hook.list.setData.call(self.hook.list, data[0]); + } self.notLoading(); }); }, @@ -92,6 +95,8 @@ require('../window')(function(w){ clearTimeout(this.timeout); } + this.destroyed = true; + this.hook.trigger.removeEventListener('keydown.dl', this.debounceTriggerWrapper); this.hook.trigger.removeEventListener('focus', this.debounceTriggerWrapper); } -- cgit v1.2.1 From 99ffd0d44e0c813f9c4216b01fa321a5409ed360 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 13:35:39 -0600 Subject: Remove ajax clear setData for smoother ux --- app/assets/javascripts/droplab/droplab_ajax_filter.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab_ajax_filter.js b/app/assets/javascripts/droplab/droplab_ajax_filter.js index 0d6a7892bdc..7603556d2ef 100644 --- a/app/assets/javascripts/droplab/droplab_ajax_filter.js +++ b/app/assets/javascripts/droplab/droplab_ajax_filter.js @@ -51,8 +51,6 @@ require('../window')(function(w){ this.loading = true; - this.hook.list.setData([]); - var params = config.params || {}; params[config.searchKey] = searchValue; var self = this; -- cgit v1.2.1 From 85f1793590b31171bac3c1e1f5d4420d96302551 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 13:47:36 -0600 Subject: Pass project ID through the DOM --- app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 | 2 +- app/assets/javascripts/filtered_search/dropdown_author.js.es6 | 2 +- .../javascripts/filtered_search/filtered_search_dropdown.js.es6 | 4 ++++ app/views/shared/issuable/_search_bar.html.haml | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 index 3420397edda..ff3fd3a4e2b 100644 --- a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 @@ -13,7 +13,7 @@ params: { per_page: 20, active: true, - project_id: 2, + project_id: this.getProjectId(), current_user: true, }, searchValueFunction: this.getSearchInput, diff --git a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 index f1401f6f9d2..517cbab8ee7 100644 --- a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 @@ -13,7 +13,7 @@ params: { per_page: 20, active: true, - project_id: 2, + project_id: this.getProjectId(), current_user: true, }, searchValueFunction: this.getSearchInput, diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 index 5186c15cb67..478c4e6bf92 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -21,6 +21,10 @@ this.dropdown.removeEventListener('click.dl', this.itemClickedWrapper); } + getProjectId() { + return this.input.getAttribute('data-project-id'); + } + getCurrentHook() { return this.droplab.hooks.filter(h => h.id === this.hookId)[0]; } diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 2d2ecf030a8..86692e77697 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -12,7 +12,7 @@ class: "check_all_issues left" .issues-other-filters.filtered-search-container .filtered-search-input-container - %input.form-control.filtered-search{ placeholder: 'Search or filter results...', 'data-id' => 'filtered-search' } + %input.form-control.filtered-search{ placeholder: 'Search or filter results...', 'data-id' => 'filtered-search', 'data-project-id' => @project.id } = icon('filter') %button.clear-search.hidden{ type: 'button' } = icon('times') -- cgit v1.2.1 From 575d4491cac225544081135820a1fa53a72d4709 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 13:55:41 -0600 Subject: Fixed bug where filters were not being reset after being cleared --- .../javascripts/filtered_search/filtered_search_manager.js.es6 | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index c7e01fc710d..5d38a23d9fd 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -247,15 +247,16 @@ this.filteredSearchInput.value = ''; this.clearSearchButton.classList.add('hidden'); - // Reset Filters - this.mapping[this.currentDropdown].reference.resetFilters(); - // Force dropdown to hide + // Force current dropdown to hide this.mapping[this.currentDropdown].reference.hideDropdown(); // Re-Load dropdown this.setDropdown(); + // Reset filters for current dropdown + this.mapping[this.currentDropdown].reference.resetFilters(); + // Reposition dropdown so that it is aligned with cursor this.updateDropdownOffset(this.currentDropdown); } -- cgit v1.2.1 From a7ecbf7c79a4789d10493ad3bdb9b02cfd124334 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 14:04:16 -0600 Subject: Add missing space for extracting params --- app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 5d38a23d9fd..055f229cd45 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -33,6 +33,7 @@ if (validCondition) { inputValue += `${validCondition.key}:${validCondition.conditions[conditionIndex].keyword}`; + inputValue += ' '; } else { // Sanitize value since URL converts spaces into + // Replace before decode so that we know what was originally + versus the encoded + -- cgit v1.2.1 From cf3504ed602aa1b979f13a394192dde50b8cffed Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 14:14:21 -0600 Subject: Make ajax filter more consistent and only filter when typed --- app/assets/javascripts/droplab/droplab_ajax_filter.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab_ajax_filter.js b/app/assets/javascripts/droplab/droplab_ajax_filter.js index 7603556d2ef..f2720a0371b 100644 --- a/app/assets/javascripts/droplab/droplab_ajax_filter.js +++ b/app/assets/javascripts/droplab/droplab_ajax_filter.js @@ -12,7 +12,7 @@ require('../window')(function(w){ this.debounceTriggerWrapper = this.debounceTrigger.bind(this); this.hook.trigger.addEventListener('keydown.dl', this.debounceTriggerWrapper); this.hook.trigger.addEventListener('focus', this.debounceTriggerWrapper); - this.trigger(); + this.trigger(true); }, notLoading: function notLoading() { @@ -22,6 +22,7 @@ require('../window')(function(w){ debounceTrigger: function debounceTrigger(e) { var NON_CHARACTER_KEYS = [16, 17, 18, 20, 37, 38, 39, 40, 91, 93]; var invalidKeyPressed = NON_CHARACTER_KEYS.indexOf(e.detail.which || e.detail.keyCode) > -1; + var focusEvent = false; if (invalidKeyPressed || this.loading) { return; } @@ -30,10 +31,14 @@ require('../window')(function(w){ clearTimeout(this.timeout); } - this.timeout = setTimeout(this.trigger.bind(this), 200); + if (e.type === 'focus') { + focusEvent = true; + } + + this.timeout = setTimeout(this.trigger.bind(this, focusEvent), 200); }, - trigger: function trigger() { + trigger: function trigger(getEntireList = false) { var config = this.hook.config.droplabAjaxFilter; var searchValue = this.trigger.value; @@ -45,6 +50,10 @@ require('../window')(function(w){ searchValue = config.searchValueFunction(); } + if (getEntireList) { + searchValue = ''; + } + if (searchValue === config.searchKey) { return this.list.show(); } -- cgit v1.2.1 From efb668208ae25393cd5535ea769c537c55a54313 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 15:37:26 -0600 Subject: Add loading message for droplab_ajax --- app/assets/javascripts/droplab/droplab_ajax.js | 15 +++++++++++++++ .../javascripts/filtered_search/dropdown_label.js.es6 | 4 ++++ .../javascripts/filtered_search/dropdown_milestone.js.es6 | 4 ++++ app/assets/stylesheets/framework/filters.scss | 4 ++++ 4 files changed, 27 insertions(+) diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js index b81663c281d..629260006f3 100644 --- a/app/assets/javascripts/droplab/droplab_ajax.js +++ b/app/assets/javascripts/droplab/droplab_ajax.js @@ -23,6 +23,7 @@ require('../window')(function(w){ }, init: function init(hook) { + var self = this; var config = hook.config.droplabAjax; if (!config || !config.endpoint || !config.method) { @@ -33,7 +34,21 @@ require('../window')(function(w){ return; } + if (config.loadingTemplate) { + var dynamicList = hook.list.list.querySelector('[data-dynamic]'); + + var loadingTemplate = document.createElement('div'); + loadingTemplate.innerHTML = config.loadingTemplate; + loadingTemplate.setAttribute('data-loading-template', true); + + this.listTemplate = dynamicList.outerHTML; + dynamicList.outerHTML = loadingTemplate.outerHTML; + } + this._loadUrlData(config.endpoint).then(function(d) { + if (config.loadingTemplate) { + hook.list.list.querySelector('[data-loading-template]').outerHTML = self.listTemplate; + } hook.list[config.method].call(hook.list, d); }).catch(function(e) { if(e.message) { diff --git a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 index 4c9926c1f78..0912336b6cf 100644 --- a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 @@ -10,6 +10,10 @@ droplabAjax: { endpoint: 'labels.json', method: 'setData', + loadingTemplate: ` +
+ +
`, }, droplabFilter: { filterFunction: this.filterWithSymbol.bind(this, '~'), diff --git a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 index 33967ddff24..73d67573868 100644 --- a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 @@ -10,6 +10,10 @@ droplabAjax: { endpoint: 'milestones.json', method: 'setData', + loadingTemplate: ` +
+ +
`, }, droplabFilter: { filterFunction: this.filterWithSymbol.bind(this, '%'), diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 205cecb4906..b6c137d647a 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -121,3 +121,7 @@ .hint-dropdown { width: 250px; } + +.filter-dropdown-loading { + padding: 8px 16px; +} -- cgit v1.2.1 From 5066366162cc1da1004bc0d3df4a6377d68dbce4 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 15:37:47 -0600 Subject: Add loading template to droplab_ajax_filter --- .../javascripts/droplab/droplab_ajax_filter.js | 24 +++++++++++++++++++++- .../filtered_search/dropdown_assignee.js.es6 | 4 ++++ .../filtered_search/dropdown_author.js.es6 | 4 ++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/droplab/droplab_ajax_filter.js b/app/assets/javascripts/droplab/droplab_ajax_filter.js index f2720a0371b..8d024c4b6d7 100644 --- a/app/assets/javascripts/droplab/droplab_ajax_filter.js +++ b/app/assets/javascripts/droplab/droplab_ajax_filter.js @@ -50,6 +50,18 @@ require('../window')(function(w){ searchValue = config.searchValueFunction(); } + if (config.loadingTemplate && this.hook.list.data === undefined || + this.hook.list.data.length === 0) { + var dynamicList = this.hook.list.list.querySelector('[data-dynamic]'); + + var loadingTemplate = document.createElement('div'); + loadingTemplate.innerHTML = config.loadingTemplate; + loadingTemplate.setAttribute('data-loading-template', true); + + this.listTemplate = dynamicList.outerHTML; + dynamicList.outerHTML = loadingTemplate.outerHTML; + } + if (getEntireList) { searchValue = ''; } @@ -64,8 +76,18 @@ require('../window')(function(w){ params[config.searchKey] = searchValue; var self = this; this._loadUrlData(config.endpoint + this.buildParams(params)).then(function(data) { + if (config.loadingTemplate && self.hook.list.data === undefined || + self.hook.list.data.length === 0) { + self.hook.list.list.querySelector('[data-loading-template]').outerHTML = self.listTemplate; + } + if (!self.destroyed) { - self.hook.list.setData.call(self.hook.list, data[0]); + if (data[0].length === 0) { + self.hook.list.hide(); + } else { + self.hook.list.show(); + self.hook.list.setData.call(self.hook.list, data[0]); + } } self.notLoading(); }); diff --git a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 index ff3fd3a4e2b..edc717304b2 100644 --- a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 @@ -17,6 +17,10 @@ current_user: true, }, searchValueFunction: this.getSearchInput, + loadingTemplate: ` +
+ +
`, } }; } diff --git a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 index 517cbab8ee7..8d95a879c79 100644 --- a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 @@ -17,6 +17,10 @@ current_user: true, }, searchValueFunction: this.getSearchInput, + loadingTemplate: ` +
+ +
`, } }; } -- cgit v1.2.1 From 0791116f5f6a6f3622af6d1caa9ba9da1818275f Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 15:40:12 -0600 Subject: Refactor loadingTemplate to abstract class --- app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 | 5 +---- app/assets/javascripts/filtered_search/dropdown_author.js.es6 | 5 +---- app/assets/javascripts/filtered_search/dropdown_label.js.es6 | 5 +---- app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 | 5 +---- .../javascripts/filtered_search/filtered_search_dropdown.js.es6 | 3 +++ 5 files changed, 7 insertions(+), 16 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 index edc717304b2..0ce4eebedc9 100644 --- a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 @@ -17,10 +17,7 @@ current_user: true, }, searchValueFunction: this.getSearchInput, - loadingTemplate: ` -
- -
`, + loadingTemplate: this.loadingTemplate, } }; } diff --git a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 index 8d95a879c79..3dc649cc17d 100644 --- a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 @@ -17,10 +17,7 @@ current_user: true, }, searchValueFunction: this.getSearchInput, - loadingTemplate: ` -
- -
`, + loadingTemplate: this.loadingTemplate, } }; } diff --git a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 index 0912336b6cf..bf009454de5 100644 --- a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 @@ -10,10 +10,7 @@ droplabAjax: { endpoint: 'labels.json', method: 'setData', - loadingTemplate: ` -
- -
`, + loadingTemplate: this.loadingTemplate, }, droplabFilter: { filterFunction: this.filterWithSymbol.bind(this, '~'), diff --git a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 index 73d67573868..7f5822aed84 100644 --- a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 @@ -10,10 +10,7 @@ droplabAjax: { endpoint: 'milestones.json', method: 'setData', - loadingTemplate: ` -
- -
`, + loadingTemplate: this.loadingTemplate, }, droplabFilter: { filterFunction: this.filterWithSymbol.bind(this, '%'), diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 index 478c4e6bf92..6b713a7017e 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -9,6 +9,9 @@ this.hookId = 'filtered-search'; this.input = input; this.dropdown = dropdown; + this.loadingTemplate = `
+ +
`; this.bindEvents(); } -- cgit v1.2.1 From f67316a7cb19374812bfc61dc98ece3110538e1b Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 16:04:58 -0600 Subject: Hide list if it is dynamic and there are no items to render --- app/assets/javascripts/droplab/droplab_ajax_filter.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab_ajax_filter.js b/app/assets/javascripts/droplab/droplab_ajax_filter.js index 8d024c4b6d7..bdd9b059bb3 100644 --- a/app/assets/javascripts/droplab/droplab_ajax_filter.js +++ b/app/assets/javascripts/droplab/droplab_ajax_filter.js @@ -82,12 +82,16 @@ require('../window')(function(w){ } if (!self.destroyed) { - if (data[0].length === 0) { + var hookListChildren = self.hook.list.list.children; + var onlyDynamicList = hookListChildren.length === 1 && hookListChildren[0].hasAttribute('data-dynamic'); + + if (onlyDynamicList && data[0].length === 0) { self.hook.list.hide(); - } else { + } else if (onlyDynamicList && data[0].length !== 0) { self.hook.list.show(); - self.hook.list.setData.call(self.hook.list, data[0]); } + + self.hook.list.setData.call(self.hook.list, data[0]); } self.notLoading(); }); -- cgit v1.2.1 From 7eb888e60e506ab25ba3593cde53dc94c19b9d7e Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 16:06:45 -0600 Subject: Only return data response for droplab ajax filter --- app/assets/javascripts/droplab/droplab_ajax_filter.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab_ajax_filter.js b/app/assets/javascripts/droplab/droplab_ajax_filter.js index bdd9b059bb3..943ee9fa0a4 100644 --- a/app/assets/javascripts/droplab/droplab_ajax_filter.js +++ b/app/assets/javascripts/droplab/droplab_ajax_filter.js @@ -85,13 +85,13 @@ require('../window')(function(w){ var hookListChildren = self.hook.list.list.children; var onlyDynamicList = hookListChildren.length === 1 && hookListChildren[0].hasAttribute('data-dynamic'); - if (onlyDynamicList && data[0].length === 0) { + if (onlyDynamicList && data.length === 0) { self.hook.list.hide(); - } else if (onlyDynamicList && data[0].length !== 0) { + } else if (onlyDynamicList && data.length !== 0) { self.hook.list.show(); } - self.hook.list.setData.call(self.hook.list, data[0]); + self.hook.list.setData.call(self.hook.list, data); } self.notLoading(); }); @@ -105,7 +105,7 @@ require('../window')(function(w){ if(xhr.readyState === XMLHttpRequest.DONE) { if (xhr.status === 200) { var data = JSON.parse(xhr.responseText); - return resolve([data, xhr]); + return resolve(data); } else { return reject([xhr.responseText, xhr.status]); } -- cgit v1.2.1 From 172ce7001ae606f7f0df56aedc6c5d6ad1d9305c Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 18:42:20 -0600 Subject: Fix casing and add upcoming milestone filter --- app/views/shared/issuable/_search_bar.html.haml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 86692e77697..7ebc4d6b153 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -45,7 +45,7 @@ %ul{ 'data-dropdown' => true } %li.filter-dropdown-item{ 'data-value': 'none' } %button.btn.btn-link - No assignee + No Assignee %li.divider %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item @@ -60,7 +60,10 @@ %ul{ 'data-dropdown' => true } %li.filter-dropdown-item{ 'data-value': 'none' } %button.btn.btn-link - No milestone + No Milestone + %li.filter-dropdown-item{ 'data-value': 'upcoming' } + %button.btn.btn-link + Upcoming %li.divider %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item @@ -70,7 +73,7 @@ %ul{ 'data-dropdown' => true } %li.filter-dropdown-item{ 'data-value': 'none' } %button.btn.btn-link - No label + No Label %li.divider %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item -- cgit v1.2.1 From acfe967eb587078672953971673e2cd2c02b43ee Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 18:53:56 -0600 Subject: Add mobile viewport --- app/assets/stylesheets/framework/mobile.scss | 6 +++++- app/views/shared/issuable/_search_bar.html.haml | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 7eb9962ba33..db3bf9f86c4 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -23,12 +23,16 @@ margin-right: 0; } - .issues-details-filters, + .issues-details-filters:not(.filtered-search-block), .dash-projects-filters, .check-all-holder { display: none; } + .issues-holder .issue-check { + display: none; + } + .rss-btn { display: none; } diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 7ebc4d6b153..f7c72e3ced8 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -2,7 +2,7 @@ - boards_page = controller.controller_name == 'boards' .issues-filters - .issues-details-filters.row-content-block.second-block + .issues-details-filters.row-content-block.second-block.filtered-search-block = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do - if params[:search].present? = hidden_field_tag :search, params[:search] -- cgit v1.2.1 From 5c0802deae6a3a87e7a497d3250cbeb98e61045b Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sun, 11 Dec 2016 18:05:33 -0600 Subject: Add check in case the data attribute does not exist --- app/assets/javascripts/droplab/droplab_ajax.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js index 629260006f3..ebb518eeef4 100644 --- a/app/assets/javascripts/droplab/droplab_ajax.js +++ b/app/assets/javascripts/droplab/droplab_ajax.js @@ -47,7 +47,11 @@ require('../window')(function(w){ this._loadUrlData(config.endpoint).then(function(d) { if (config.loadingTemplate) { - hook.list.list.querySelector('[data-loading-template]').outerHTML = self.listTemplate; + var dataLoadingTemplate = hook.list.list.querySelector('[data-loading-template]'); + + if (dataLoadingTemplate) { + dataLoadingTemplate.outerHTML = self.listTemplate; + } } hook.list[config.method].call(hook.list, d); }).catch(function(e) { -- cgit v1.2.1 From 46a1f36986aa61597f54d14c76b1d2258b0933e5 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sun, 11 Dec 2016 18:05:55 -0600 Subject: Refactor dropdown filters --- .../filtered_search/dropdown_assignee.js.es6 | 61 ---------------------- .../filtered_search/dropdown_author.js.es6 | 57 -------------------- .../filtered_search/dropdown_hint.js.es6 | 2 +- .../filtered_search/dropdown_label.js.es6 | 45 ---------------- .../filtered_search/dropdown_milestone.js.es6 | 44 ---------------- .../filtered_search/dropdown_non_user.js.es6 | 44 ++++++++++++++++ .../filtered_search/dropdown_user.js.es6 | 57 ++++++++++++++++++++ .../filtered_search/filtered_search_manager.js.es6 | 16 ++++-- app/views/shared/issuable/_search_bar.html.haml | 4 +- 9 files changed, 115 insertions(+), 215 deletions(-) delete mode 100644 app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 delete mode 100644 app/assets/javascripts/filtered_search/dropdown_author.js.es6 delete mode 100644 app/assets/javascripts/filtered_search/dropdown_label.js.es6 delete mode 100644 app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 create mode 100644 app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 create mode 100644 app/assets/javascripts/filtered_search/dropdown_user.js.es6 diff --git a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 deleted file mode 100644 index 0ce4eebedc9..00000000000 --- a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 +++ /dev/null @@ -1,61 +0,0 @@ -/* eslint-disable no-param-reassign */ -/*= require filtered_search/filtered_search_dropdown */ - -((global) => { - class DropdownAssignee extends gl.FilteredSearchDropdown { - constructor(droplab, dropdown, input) { - super(droplab, dropdown, input); - this.listId = 'js-dropdown-assignee'; - this.config = { - droplabAjaxFilter: { - endpoint: '/autocomplete/users.json', - searchKey: 'search', - params: { - per_page: 20, - active: true, - project_id: this.getProjectId(), - current_user: true, - }, - searchValueFunction: this.getSearchInput, - loadingTemplate: this.loadingTemplate, - } - }; - } - - itemClicked(e) { - const dataValueSet = this.setDataValueIfSelected(e.detail.selected); - - if (!dataValueSet) { - const username = e.detail.selected.querySelector('.dropdown-light-content').innerText.trim(); - gl.FilteredSearchManager.addWordToInput(this.getSelectedText(username)); - } - - this.dismissDropdown(!dataValueSet); - } - - renderContent(forceShowList = false) { - this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjaxFilter], this.config); - super.renderContent(forceShowList); - } - - getSearchInput() { - const query = document.querySelector('.filtered-search').value; - const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); - const valueWithoutColon = value.slice(1); - const hasPrefix = valueWithoutColon[0] === '@'; - const valueWithoutPrefix = valueWithoutColon.slice(1); - - if (hasPrefix) { - return valueWithoutPrefix; - } else { - return valueWithoutColon; - } - } - - configure() { - this.droplab.addHook(this.input, this.dropdown, [droplabAjaxFilter], this.config).init(); - } - } - - global.DropdownAssignee = DropdownAssignee; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 deleted file mode 100644 index 3dc649cc17d..00000000000 --- a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 +++ /dev/null @@ -1,57 +0,0 @@ -/* eslint-disable no-param-reassign */ -/*= require filtered_search/filtered_search_dropdown */ - -((global) => { - class DropdownAuthor extends gl.FilteredSearchDropdown { - constructor(droplab, dropdown, input) { - super(droplab, dropdown, input); - this.listId = 'js-dropdown-author'; - this.config = { - droplabAjaxFilter: { - endpoint: '/autocomplete/users.json', - searchKey: 'search', - params: { - per_page: 20, - active: true, - project_id: this.getProjectId(), - current_user: true, - }, - searchValueFunction: this.getSearchInput, - loadingTemplate: this.loadingTemplate, - } - }; - } - - itemClicked(e) { - const username = e.detail.selected.querySelector('.dropdown-light-content').innerText.trim(); - gl.FilteredSearchManager.addWordToInput(this.getSelectedText(username)); - - this.dismissDropdown(); - } - - renderContent(forceShowList) { - this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjaxFilter], this.config); - super.renderContent(forceShowList); - } - - getSearchInput() { - const query = document.querySelector('.filtered-search').value; - const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); - const valueWithoutColon = value.slice(1); - const hasPrefix = valueWithoutColon[0] === '@'; - const valueWithoutPrefix = valueWithoutColon.slice(1); - - if (hasPrefix) { - return valueWithoutPrefix; - } else { - return valueWithoutColon; - } - } - - configure() { - this.droplab.addHook(this.input, this.dropdown, [droplabAjaxFilter], this.config).init(); - } - } - - global.DropdownAuthor = DropdownAuthor; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index ea384af09a9..d445a796f43 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -23,7 +23,7 @@ class DropdownHint extends gl.FilteredSearchDropdown { constructor(droplab, dropdown, input) { super(droplab, dropdown, input); - this.listId = 'js-dropdown-hint'; + this.listId = dropdown.id; this.config = { droplabFilter: { template: 'hint', diff --git a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 deleted file mode 100644 index bf009454de5..00000000000 --- a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 +++ /dev/null @@ -1,45 +0,0 @@ -/* eslint-disable no-param-reassign */ -/*= require filtered_search/filtered_search_dropdown */ - -((global) => { - class DropdownLabel extends gl.FilteredSearchDropdown { - constructor(droplab, dropdown, input) { - super(droplab, dropdown, input); - this.listId = 'js-dropdown-label'; - this.config = { - droplabAjax: { - endpoint: 'labels.json', - method: 'setData', - loadingTemplate: this.loadingTemplate, - }, - droplabFilter: { - filterFunction: this.filterWithSymbol.bind(this, '~'), - } - }; - } - - itemClicked(e) { - const dataValueSet = this.setDataValueIfSelected(e.detail.selected); - - if (!dataValueSet) { - const labelTitle = e.detail.selected.querySelector('.label-title').innerText.trim(); - const labelName = `~${this.getEscapedText(labelTitle)}`; - gl.FilteredSearchManager.addWordToInput(labelName); - } - - // debugger - this.dismissDropdown(!dataValueSet); - } - - renderContent(forceShowList) { - this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjax, droplabFilter], this.config); - super.renderContent(forceShowList); - } - - configure() { - this.droplab.addHook(this.input, this.dropdown, [droplabAjax, droplabFilter], this.config).init(); - } - } - - global.DropdownLabel = DropdownLabel; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 deleted file mode 100644 index 7f5822aed84..00000000000 --- a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 +++ /dev/null @@ -1,44 +0,0 @@ -/* eslint-disable no-param-reassign */ -/*= require filtered_search/filtered_search_dropdown */ - -((global) => { - class DropdownMilestone extends gl.FilteredSearchDropdown { - constructor(droplab, dropdown, input) { - super(droplab, dropdown, input); - this.listId = 'js-dropdown-milestone'; - this.config = { - droplabAjax: { - endpoint: 'milestones.json', - method: 'setData', - loadingTemplate: this.loadingTemplate, - }, - droplabFilter: { - filterFunction: this.filterWithSymbol.bind(this, '%'), - } - }; - } - - itemClicked(e) { - const dataValueSet = this.setDataValueIfSelected(e.detail.selected); - - if (!dataValueSet) { - const milestoneTitle = e.detail.selected.querySelector('.btn-link').innerText.trim(); - const milestoneName = `%${this.getEscapedText(milestoneTitle)}`; - gl.FilteredSearchManager.addWordToInput(this.getSelectedText(milestoneName)); - } - - this.dismissDropdown(!dataValueSet); - } - - renderContent(forceShowList = false) { - this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjax, droplabFilter], this.config); - super.renderContent(forceShowList); - } - - configure() { - this.droplab.addHook(this.input, this.dropdown, [droplabAjax, droplabFilter], this.config).init(); - } - } - - global.DropdownMilestone = DropdownMilestone; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 new file mode 100644 index 00000000000..05c9284bc96 --- /dev/null +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 @@ -0,0 +1,44 @@ +/* eslint-disable no-param-reassign */ +/*= require filtered_search/filtered_search_dropdown */ + +((global) => { + class DropdownNonUser extends gl.FilteredSearchDropdown { + constructor(droplab, dropdown, input, endpoint, symbol) { + super(droplab, dropdown, input); + this.listId = dropdown.id; + this.config = { + droplabAjax: { + endpoint: endpoint, + method: 'setData', + loadingTemplate: this.loadingTemplate, + }, + droplabFilter: { + filterFunction: this.filterWithSymbol.bind(this, symbol), + } + }; + } + + itemClicked(e) { + const dataValueSet = this.setDataValueIfSelected(e.detail.selected); + + if (!dataValueSet) { + const title = e.detail.selected.querySelector('.js-data-value').innerText.trim(); + const name = `%${this.getEscapedText(title)}`; + gl.FilteredSearchManager.addWordToInput(this.getSelectedText(name)); + } + + this.dismissDropdown(!dataValueSet); + } + + renderContent(forceShowList = false) { + this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjax, droplabFilter], this.config); + super.renderContent(forceShowList); + } + + configure() { + this.droplab.addHook(this.input, this.dropdown, [droplabAjax, droplabFilter], this.config).init(); + } + } + + global.DropdownNonUser = DropdownNonUser; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 new file mode 100644 index 00000000000..1a597bbbc9d --- /dev/null +++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 @@ -0,0 +1,57 @@ +/* eslint-disable no-param-reassign */ +/*= require filtered_search/filtered_search_dropdown */ + +((global) => { + class DropdownUser extends gl.FilteredSearchDropdown { + constructor(droplab, dropdown, input) { + super(droplab, dropdown, input); + this.listId = dropdown.id; + this.config = { + droplabAjaxFilter: { + endpoint: '/autocomplete/users.json', + searchKey: 'search', + params: { + per_page: 20, + active: true, + project_id: this.getProjectId(), + current_user: true, + }, + searchValueFunction: this.getSearchInput, + loadingTemplate: this.loadingTemplate, + }, + }; + } + + itemClicked(e) { + const dataValueSet = this.setDataValueIfSelected(e.detail.selected); + + if (!dataValueSet) { + const username = e.detail.selected.querySelector('.dropdown-light-content').innerText.trim(); + gl.FilteredSearchManager.addWordToInput(this.getSelectedText(username)); + } + + this.dismissDropdown(!dataValueSet); + } + + renderContent(forceShowList = false) { + this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjaxFilter], this.config); + super.renderContent(forceShowList); + } + + getSearchInput() { + const query = document.querySelector('.filtered-search').value; + const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); + const valueWithoutColon = value.slice(1); + const hasPrefix = valueWithoutColon[0] === '@'; + const valueWithoutPrefix = valueWithoutColon.slice(1); + + return hasPrefix ? valueWithoutPrefix : valueWithoutColon; + } + + configure() { + this.droplab.addHook(this.input, this.dropdown, [droplabAjaxFilter], this.config).init(); + } + } + + global.DropdownUser = DropdownUser; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 055f229cd45..c92d669114e 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -103,22 +103,24 @@ this.mapping = { author: { reference: null, - gl: 'DropdownAuthor', + gl: 'DropdownUser', element: document.querySelector('#js-dropdown-author'), }, assignee: { reference: null, - gl: 'DropdownAssignee', + gl: 'DropdownUser', element: document.querySelector('#js-dropdown-assignee'), }, milestone: { reference: null, - gl: 'DropdownMilestone', + gl: 'DropdownNonUser', + extraArguments: ['milestones.json', '%'], element: document.querySelector('#js-dropdown-milestone'), }, label: { reference: null, - gl: 'DropdownLabel', + gl: 'DropdownNonUser', + extraArguments: ['labels.json', '~'], element: document.querySelector('#js-dropdown-label'), }, hint: { @@ -160,7 +162,11 @@ let forceShowList = false; if (!this.mapping[key].reference) { - this.mapping[key].reference = new gl[glClass](this.droplab, element, this.filteredSearchInput); + var dl = this.droplab; + const defaultArguments = [null, dl, element, this.filteredSearchInput]; + const glArguments = defaultArguments.concat(this.mapping[key].extraArguments || []); + + this.mapping[key].reference = new (Function.prototype.bind.apply(gl[glClass], glArguments)); } if (firstLoad) { diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index f7c72e3ced8..335552c0a26 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -67,7 +67,7 @@ %li.divider %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item - %button.btn.btn-link + %button.btn.btn-link.js-data-value {{title}} #js-dropdown-label.dropdown-menu{ 'data-dropdown' => true } %ul{ 'data-dropdown' => true } @@ -79,7 +79,7 @@ %li.filter-dropdown-item %button.btn.btn-link %span.dropdown-label-box{ 'style': 'background: {{color}}'} - %span.label-title + %span.label-title.js-data-value {{title}} .pull-right - if boards_page -- cgit v1.2.1 From f4db75728e8c16876cb3f74e12d4d707ab8f47c1 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sun, 11 Dec 2016 18:11:58 -0600 Subject: Refactor filtered_search_dropdown --- .../filtered_search/dropdown_non_user.js.es6 | 39 ++++++++++++++++++++-- .../filtered_search/dropdown_user.js.es6 | 4 +++ .../filtered_search_dropdown.js.es6 | 39 ---------------------- 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 index 05c9284bc96..f03c27c3ec0 100644 --- a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 @@ -6,6 +6,7 @@ constructor(droplab, dropdown, input, endpoint, symbol) { super(droplab, dropdown, input); this.listId = dropdown.id; + this.symbol = symbol; this.config = { droplabAjax: { endpoint: endpoint, @@ -13,7 +14,7 @@ loadingTemplate: this.loadingTemplate, }, droplabFilter: { - filterFunction: this.filterWithSymbol.bind(this, symbol), + filterFunction: this.filterWithSymbol.bind(this, this.symbol), } }; } @@ -23,13 +24,47 @@ if (!dataValueSet) { const title = e.detail.selected.querySelector('.js-data-value').innerText.trim(); - const name = `%${this.getEscapedText(title)}`; + const name = `${this.symbol}${this.getEscapedText(title)}`; gl.FilteredSearchManager.addWordToInput(this.getSelectedText(name)); } this.dismissDropdown(!dataValueSet); } + getEscapedText(text) { + let escapedText = text; + + // Encapsulate value with quotes if it has spaces + if (text.indexOf(' ') !== -1) { + if (text.indexOf('"') !== -1) { + // Use single quotes if value contains double quotes + escapedText = `'${text}'`; + } else { + // Known side effect: values's with both single and double quotes + // won't escape properly + escapedText = `"${text}"`; + } + } + + return escapedText; + } + + filterWithSymbol(filterSymbol, item, query) { + const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); + const valueWithoutColon = value.slice(1).toLowerCase(); + const prefix = valueWithoutColon[0]; + const valueWithoutPrefix = valueWithoutColon.slice(1); + + const title = item.title.toLowerCase(); + + // Eg. filterSymbol = ~ for labels + const matchWithoutPrefix = prefix === filterSymbol && title.indexOf(valueWithoutPrefix) !== -1; + const match = title.indexOf(valueWithoutColon) !== -1; + + item.droplab_hidden = !match && !matchWithoutPrefix; + return item; + } + renderContent(forceShowList = false) { this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjax, droplabFilter], this.config); super.renderContent(forceShowList); diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 index 1a597bbbc9d..6827ab1658a 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 @@ -38,6 +38,10 @@ super.renderContent(forceShowList); } + getProjectId() { + return this.input.getAttribute('data-project-id'); + } + getSearchInput() { const query = document.querySelector('.filtered-search').value; const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 index 6b713a7017e..c63ba1acf0b 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -4,7 +4,6 @@ class FilteredSearchDropdown { constructor(droplab, dropdown, input) { - console.log('constructor'); this.droplab = droplab; this.hookId = 'filtered-search'; this.input = input; @@ -24,32 +23,10 @@ this.dropdown.removeEventListener('click.dl', this.itemClickedWrapper); } - getProjectId() { - return this.input.getAttribute('data-project-id'); - } - getCurrentHook() { return this.droplab.hooks.filter(h => h.id === this.hookId)[0]; } - getEscapedText(text) { - let escapedText = text; - - // Encapsulate value with quotes if it has spaces - if (text.indexOf(' ') !== -1) { - if (text.indexOf('"') !== -1) { - // Use single quotes if value contains double quotes - escapedText = `'${text}'`; - } else { - // Known side effect: values's with both single and double quotes - // won't escape properly - escapedText = `"${text}"`; - } - } - - return escapedText; - } - getSelectedText(selectedToken) { // TODO: Get last word from FilteredSearchTokenizer const lastWord = this.input.value.split(' ').last(); @@ -109,22 +86,6 @@ } } - filterWithSymbol(filterSymbol, item, query) { - const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); - const valueWithoutColon = value.slice(1).toLowerCase(); - const prefix = valueWithoutColon[0]; - const valueWithoutPrefix = valueWithoutColon.slice(1); - - const title = item.title.toLowerCase(); - - // Eg. filterSymbol = ~ for labels - const matchWithoutPrefix = prefix === filterSymbol && title.indexOf(valueWithoutPrefix) !== -1; - const match = title.indexOf(valueWithoutColon) !== -1; - - item.droplab_hidden = !match && !matchWithoutPrefix; - return item; - } - hideDropdown() { this.getCurrentHook().list.hide(); } -- cgit v1.2.1 From 513cdda31667f0058b24e8f66d87ddfcf89b7fb4 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 09:21:38 -0600 Subject: Refactor filtered search manager --- .../filtered_search/dropdown_hint.js.es6 | 2 +- .../filtered_search/dropdown_non_user.js.es6 | 2 +- .../filtered_search/dropdown_user.js.es6 | 2 +- .../filtered_search_dropdown.js.es6 | 2 +- .../filtered_search_dropdown_manager.js.es6 | 174 +++++++++++++++++++++ .../filtered_search/filtered_search_manager.js.es6 | 160 +------------------ 6 files changed, 185 insertions(+), 157 deletions(-) create mode 100644 app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index d445a796f43..53952e6bc63 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -42,7 +42,7 @@ const tag = selected.querySelector('.js-filter-tag').innerText.trim(); if (tag.length) { - gl.FilteredSearchManager.addWordToInput(this.getSelectedText(token)); + gl.FilteredSearchDropdownManager.addWordToInput(this.getSelectedText(token)); } this.dismissDropdown(); this.dispatchInputEvent(); diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 index f03c27c3ec0..e4df39cfde1 100644 --- a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 @@ -25,7 +25,7 @@ if (!dataValueSet) { const title = e.detail.selected.querySelector('.js-data-value').innerText.trim(); const name = `${this.symbol}${this.getEscapedText(title)}`; - gl.FilteredSearchManager.addWordToInput(this.getSelectedText(name)); + gl.FilteredSearchDropdownManager.addWordToInput(this.getSelectedText(name)); } this.dismissDropdown(!dataValueSet); diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 index 6827ab1658a..d3c3be9b914 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 @@ -27,7 +27,7 @@ if (!dataValueSet) { const username = e.detail.selected.querySelector('.dropdown-light-content').innerText.trim(); - gl.FilteredSearchManager.addWordToInput(this.getSelectedText(username)); + gl.FilteredSearchDropdownManager.addWordToInput(this.getSelectedText(username)); } this.dismissDropdown(!dataValueSet); diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 index c63ba1acf0b..38ecbbf552d 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -57,7 +57,7 @@ const dataValue = selected.getAttribute('data-value'); if (dataValue) { - gl.FilteredSearchManager.addWordToInput(dataValue); + gl.FilteredSearchDropdownManager.addWordToInput(dataValue); } return dataValue !== null; diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 new file mode 100644 index 00000000000..67a474985c0 --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 @@ -0,0 +1,174 @@ +/* eslint-disable no-param-reassign */ +((global) => { + class FilteredSearchDropdownManager { + constructor() { + this.tokenizer = gl.FilteredSearchTokenizer; + this.filteredSearchInput = document.querySelector('.filtered-search'); + + this.cleanupWrapper = this.cleanup.bind(this); + document.addEventListener('page:fetch', this.cleanupWrapper); + } + + cleanup() { + if (this.droplab) { + this.droplab.destroy(); + this.droplab = null; + } + + this.setupMapping(); + + document.removeEventListener('page:fetch', this.cleanupWrapper); + } + + setupMapping() { + this.mapping = { + author: { + reference: null, + gl: 'DropdownUser', + element: document.querySelector('#js-dropdown-author'), + }, + assignee: { + reference: null, + gl: 'DropdownUser', + element: document.querySelector('#js-dropdown-assignee'), + }, + milestone: { + reference: null, + gl: 'DropdownNonUser', + extraArguments: ['milestones.json', '%'], + element: document.querySelector('#js-dropdown-milestone'), + }, + label: { + reference: null, + gl: 'DropdownNonUser', + extraArguments: ['labels.json', '~'], + element: document.querySelector('#js-dropdown-label'), + }, + hint: { + reference: null, + gl: 'DropdownHint', + element: document.querySelector('#js-dropdown-hint'), + }, + } + } + + static addWordToInput(word, addSpace) { + const filteredSearchInput = document.querySelector('.filtered-search') + const filteredSearchValue = filteredSearchInput.value; + const hasExistingValue = filteredSearchValue.length !== 0; + const { lastToken } = gl.FilteredSearchTokenizer.processTokens(filteredSearchValue); + + if (lastToken.hasOwnProperty('key')) { + console.log(lastToken); + // Spaces inside the token means that the token value will be escaped by quotes + const hasQuotes = lastToken.value.indexOf(' ') !== -1; + const lengthToRemove = hasQuotes ? lastToken.value.length + 2 : lastToken.value.length; + filteredSearchInput.value = filteredSearchValue.slice(0, -1 * (lengthToRemove)); + } + + filteredSearchInput.value += hasExistingValue && addSpace ? ` ${word}` : word; + } + + updateCurrentDropdownOffset() { + this.updateDropdownOffset(this.currentDropdown); + } + + updateDropdownOffset(key) { + const filterIconPadding = 27; + const offset = gl.text.getTextWidth(this.filteredSearchInput.value, this.font) + filterIconPadding; + + this.mapping[key].reference.setOffset(offset); + } + + load(key, firstLoad = false) { + console.log(`🦄 load ${key} dropdown`); + const glClass = this.mapping[key].gl; + const element = this.mapping[key].element; + let forceShowList = false; + + if (!this.mapping[key].reference) { + var dl = this.droplab; + const defaultArguments = [null, dl, element, this.filteredSearchInput]; + const glArguments = defaultArguments.concat(this.mapping[key].extraArguments || []); + + this.mapping[key].reference = new (Function.prototype.bind.apply(gl[glClass], glArguments)); + } + + if (firstLoad) { + this.mapping[key].reference.configure(); + } + + if (this.currentDropdown === 'hint') { + // Clicked from hint dropdown + forceShowList = true; + } + + this.updateDropdownOffset(key); + this.mapping[key].reference.render(firstLoad, forceShowList); + + this.currentDropdown = key; + } + + loadDropdown(dropdownName = '') { + let firstLoad = false; + + if(!this.droplab) { + firstLoad = true; + this.droplab = new DropLab(); + } + + if (!this.font) { + this.font = window.getComputedStyle(this.filteredSearchInput).font; + } + + const match = gl.FilteredSearchTokenKeys.get().filter(value => value.key === dropdownName.toLowerCase())[0]; + const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key && this.mapping.hasOwnProperty(match.key); + const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint'; + + if (shouldOpenFilterDropdown || shouldOpenHintDropdown) { + const key = match && match.hasOwnProperty('key') ? match.key : 'hint'; + this.load(key, firstLoad); + } + + gl.droplab = this.droplab; + } + + setDropdown() { + const { lastToken } = this.tokenizer.processTokens(this.filteredSearchInput.value); + + if (typeof lastToken === 'string') { + // Token is not fully initialized yet + // because it has no value + // Eg. token = 'label:' + const { tokenKey } = this.tokenizer.parseToken(lastToken); + this.loadDropdown(tokenKey); + } else if (lastToken.hasOwnProperty('key')) { + // Token has been initialized into an object + // because it has a value + this.loadDropdown(lastToken.key); + } else { + this.loadDropdown('hint'); + } + } + + resetDropdowns() { + // Force current dropdown to hide + this.mapping[this.currentDropdown].reference.hideDropdown(); + + // Re-Load dropdown + this.setDropdown(); + + // Reset filters for current dropdown + this.mapping[this.currentDropdown].reference.resetFilters(); + + // Reposition dropdown so that it is aligned with cursor + this.updateDropdownOffset(this.currentDropdown); + } + + destroyDroplab() { + this.droplab.destroy(); + } + } + + global.FilteredSearchDropdownManager = FilteredSearchDropdownManager; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index c92d669114e..d9ea44b3a13 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -75,159 +75,24 @@ this.tokenizer = gl.FilteredSearchTokenizer; this.filteredSearchInput = document.querySelector('.filtered-search'); this.clearSearchButton = document.querySelector('.clear-search'); + this.dropdownManager = new gl.FilteredSearchDropdownManager(); - this.setupMapping(); + this.dropdownManager.setupMapping(); this.bindEvents(); loadSearchParamsFromURL(); - this.setDropdown(); + this.dropdownManager.setDropdown(); this.cleanupWrapper = this.cleanup.bind(this); document.addEventListener('page:fetch', this.cleanupWrapper); } cleanup() { - console.log('cleanup') - - if (this.droplab) { - this.droplab.destroy(); - this.droplab = null; - } - - this.setupMapping(); - this.unbindEvents(); document.removeEventListener('page:fetch', this.cleanupWrapper); } - setupMapping() { - this.mapping = { - author: { - reference: null, - gl: 'DropdownUser', - element: document.querySelector('#js-dropdown-author'), - }, - assignee: { - reference: null, - gl: 'DropdownUser', - element: document.querySelector('#js-dropdown-assignee'), - }, - milestone: { - reference: null, - gl: 'DropdownNonUser', - extraArguments: ['milestones.json', '%'], - element: document.querySelector('#js-dropdown-milestone'), - }, - label: { - reference: null, - gl: 'DropdownNonUser', - extraArguments: ['labels.json', '~'], - element: document.querySelector('#js-dropdown-label'), - }, - hint: { - reference: null, - gl: 'DropdownHint', - element: document.querySelector('#js-dropdown-hint'), - }, - } - } - - static addWordToInput(word, addSpace) { - const filteredSearchInput = document.querySelector('.filtered-search') - const filteredSearchValue = filteredSearchInput.value; - const hasExistingValue = filteredSearchValue.length !== 0; - const { lastToken } = gl.FilteredSearchTokenizer.processTokens(filteredSearchValue); - - if (lastToken.hasOwnProperty('key')) { - console.log(lastToken); - // Spaces inside the token means that the token value will be escaped by quotes - const hasQuotes = lastToken.value.indexOf(' ') !== -1; - const lengthToRemove = hasQuotes ? lastToken.value.length + 2 : lastToken.value.length; - filteredSearchInput.value = filteredSearchValue.slice(0, -1 * (lengthToRemove)); - } - - filteredSearchInput.value += hasExistingValue && addSpace ? ` ${word}` : word; - } - - updateDropdownOffset(key) { - const filterIconPadding = 27; - const offset = gl.text.getTextWidth(this.filteredSearchInput.value, this.font) + filterIconPadding; - - this.mapping[key].reference.setOffset(offset); - } - - load(key, firstLoad = false) { - console.log(`🦄 load ${key} dropdown`); - const glClass = this.mapping[key].gl; - const element = this.mapping[key].element; - let forceShowList = false; - - if (!this.mapping[key].reference) { - var dl = this.droplab; - const defaultArguments = [null, dl, element, this.filteredSearchInput]; - const glArguments = defaultArguments.concat(this.mapping[key].extraArguments || []); - - this.mapping[key].reference = new (Function.prototype.bind.apply(gl[glClass], glArguments)); - } - - if (firstLoad) { - this.mapping[key].reference.configure(); - } - - if (this.currentDropdown === 'hint') { - // Clicked from hint dropdown - forceShowList = true; - } - - this.updateDropdownOffset(key); - this.mapping[key].reference.render(firstLoad, forceShowList); - - this.currentDropdown = key; - } - - loadDropdown(dropdownName = '') { - let firstLoad = false; - - if(!this.droplab) { - firstLoad = true; - this.droplab = new DropLab(); - } - - if (!this.font) { - this.font = window.getComputedStyle(this.filteredSearchInput).font; - } - - const match = gl.FilteredSearchTokenKeys.get().filter(value => value.key === dropdownName.toLowerCase())[0]; - const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key && this.mapping.hasOwnProperty(match.key); - const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint'; - - if (shouldOpenFilterDropdown || shouldOpenHintDropdown) { - const key = match && match.hasOwnProperty('key') ? match.key : 'hint'; - this.load(key, firstLoad); - } - - gl.droplab = this.droplab; - } - - setDropdown() { - const { lastToken } = this.tokenizer.processTokens(this.filteredSearchInput.value); - - if (typeof lastToken === 'string') { - // Token is not fully initialized yet - // because it has no value - // Eg. token = 'label:' - const { tokenKey } = this.tokenizer.parseToken(lastToken); - this.loadDropdown(tokenKey); - } else if (lastToken.hasOwnProperty('key')) { - // Token has been initialized into an object - // because it has a value - this.loadDropdown(lastToken.key); - } else { - this.loadDropdown('hint'); - } - } - bindEvents() { - this.setDropdownWrapper = this.setDropdown.bind(this); + this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager); this.checkForEnterWrapper = this.checkForEnter.bind(this); this.clearSearchWrapper = this.clearSearch.bind(this); this.checkForBackspaceWrapper = this.checkForBackspace.bind(this); @@ -254,24 +119,13 @@ this.filteredSearchInput.value = ''; this.clearSearchButton.classList.add('hidden'); - - // Force current dropdown to hide - this.mapping[this.currentDropdown].reference.hideDropdown(); - - // Re-Load dropdown - this.setDropdown(); - - // Reset filters for current dropdown - this.mapping[this.currentDropdown].reference.resetFilters(); - - // Reposition dropdown so that it is aligned with cursor - this.updateDropdownOffset(this.currentDropdown); + this.dropdownManager.resetDropdowns(); } checkForBackspace(e) { if (e.keyCode === 8) { // Reposition dropdown so that it is aligned with cursor - this.updateDropdownOffset(this.currentDropdown); + this.dropdownManager.updateCurrentDropdownOffset(); } } @@ -282,7 +136,7 @@ e.preventDefault(); // Prevent droplab from opening dropdown - this.droplab.destroy(); + this.dropdownManager.destroyDroplab(); this.search(); } -- cgit v1.2.1 From 6c811d478d60246a8c2abf4b1bc4fd252d344ed9 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 09:47:21 -0600 Subject: Remove show() as it is automatically called on setData when there is data --- app/assets/javascripts/droplab/droplab_ajax_filter.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab_ajax_filter.js b/app/assets/javascripts/droplab/droplab_ajax_filter.js index 943ee9fa0a4..6e1eb080e3b 100644 --- a/app/assets/javascripts/droplab/droplab_ajax_filter.js +++ b/app/assets/javascripts/droplab/droplab_ajax_filter.js @@ -87,8 +87,6 @@ require('../window')(function(w){ if (onlyDynamicList && data.length === 0) { self.hook.list.hide(); - } else if (onlyDynamicList && data.length !== 0) { - self.hook.list.show(); } self.hook.list.setData.call(self.hook.list, data); -- cgit v1.2.1 From 2bbc44cb7edb81d1e83836574573b365f0b4d1cb Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 09:55:27 -0600 Subject: Refactor itemClicked --- .../javascripts/filtered_search/dropdown_non_user.js.es6 | 11 +++-------- app/assets/javascripts/filtered_search/dropdown_user.js.es6 | 11 +++-------- .../filtered_search/filtered_search_dropdown.js.es6 | 11 +++++++++-- 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 index e4df39cfde1..752a9a6e242 100644 --- a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 @@ -20,15 +20,10 @@ } itemClicked(e) { - const dataValueSet = this.setDataValueIfSelected(e.detail.selected); - - if (!dataValueSet) { + super.itemClicked(e, (selected) => { const title = e.detail.selected.querySelector('.js-data-value').innerText.trim(); - const name = `${this.symbol}${this.getEscapedText(title)}`; - gl.FilteredSearchDropdownManager.addWordToInput(this.getSelectedText(name)); - } - - this.dismissDropdown(!dataValueSet); + return `${this.symbol}${this.getEscapedText(title)}`; + }); } getEscapedText(text) { diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 index d3c3be9b914..749fb9d90aa 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 @@ -23,14 +23,9 @@ } itemClicked(e) { - const dataValueSet = this.setDataValueIfSelected(e.detail.selected); - - if (!dataValueSet) { - const username = e.detail.selected.querySelector('.dropdown-light-content').innerText.trim(); - gl.FilteredSearchDropdownManager.addWordToInput(this.getSelectedText(username)); - } - - this.dismissDropdown(!dataValueSet); + super.itemClicked(e, (selected) => { + return selected.querySelector('.dropdown-light-content').innerText.trim(); + }); } renderContent(forceShowList = false) { diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 index 38ecbbf552d..990d56188cb 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -35,8 +35,15 @@ return lastWordIndex === -1 ? selectedToken : selectedToken.slice(lastWord.length); } - itemClicked(e) { - // Overridden by dropdown sub class + itemClicked(e, getValueFunction) { + const dataValueSet = this.setDataValueIfSelected(e.detail.selected); + + if (!dataValueSet) { + const value = getValueFunction(e.detail.selected) + gl.FilteredSearchDropdownManager.addWordToInput(this.getSelectedText(value)); + } + + this.dismissDropdown(); } renderContent(forceShowList = false) { -- cgit v1.2.1 From 5589ab1e0be2d682a8be424289d17b4e566caba0 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 10:28:22 -0600 Subject: Refactor and add comments --- .../filtered_search/dropdown_hint.js.es6 | 3 +- .../filtered_search/dropdown_non_user.js.es6 | 17 ++++----- .../filtered_search/dropdown_user.js.es6 | 3 +- .../filtered_search_dropdown.js.es6 | 39 ++++++++++---------- .../filtered_search_dropdown_manager.js.es6 | 41 ++++++++++++---------- .../filtered_search/filtered_search_manager.js.es6 | 9 ++--- .../filtered_search/filtered_search_tokenizer.es6 | 12 ------- 7 files changed, 58 insertions(+), 66 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index 53952e6bc63..43a0b1da0fe 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -23,7 +23,6 @@ class DropdownHint extends gl.FilteredSearchDropdown { constructor(droplab, dropdown, input) { super(droplab, dropdown, input); - this.listId = dropdown.id; this.config = { droplabFilter: { template: 'hint', @@ -66,7 +65,7 @@ return item; } - configure() { + init() { this.droplab.addHook(this.input, this.dropdown, [droplabFilter], this.config).init(); } } diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 index 752a9a6e242..0969df65836 100644 --- a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 @@ -5,7 +5,6 @@ class DropdownNonUser extends gl.FilteredSearchDropdown { constructor(droplab, dropdown, input, endpoint, symbol) { super(droplab, dropdown, input); - this.listId = dropdown.id; this.symbol = symbol; this.config = { droplabAjax: { @@ -28,15 +27,17 @@ getEscapedText(text) { let escapedText = text; + const hasSpace = text.indexOf(' ') !== -1; + const hasDoubleQuote = text.indexOf('"') !== -1; + const hasSingleQuote = text.indexOf('\'') !== -1; // Encapsulate value with quotes if it has spaces - if (text.indexOf(' ') !== -1) { - if (text.indexOf('"') !== -1) { - // Use single quotes if value contains double quotes + // Known side effect: values's with both single and double quotes + // won't escape properly + if (hasSpace) { + if (hasDoubleQuote) { escapedText = `'${text}'`; - } else { - // Known side effect: values's with both single and double quotes - // won't escape properly + } else if (hasSingleQuote) { escapedText = `"${text}"`; } } @@ -65,7 +66,7 @@ super.renderContent(forceShowList); } - configure() { + init() { this.droplab.addHook(this.input, this.dropdown, [droplabAjax, droplabFilter], this.config).init(); } } diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 index 749fb9d90aa..8bc274e0b12 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 @@ -5,7 +5,6 @@ class DropdownUser extends gl.FilteredSearchDropdown { constructor(droplab, dropdown, input) { super(droplab, dropdown, input); - this.listId = dropdown.id; this.config = { droplabAjaxFilter: { endpoint: '/autocomplete/users.json', @@ -47,7 +46,7 @@ return hasPrefix ? valueWithoutPrefix : valueWithoutColon; } - configure() { + init() { this.droplab.addHook(this.input, this.dropdown, [droplabAjaxFilter], this.config).init(); } } diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 index 990d56188cb..85d684e3058 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -46,14 +46,8 @@ this.dismissDropdown(); } - renderContent(forceShowList = false) { - if (forceShowList && this.getCurrentHook().list.hidden) { - this.getCurrentHook().list.show(); - } - } - setAsDropdown() { - this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.listId}`); + this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.dropdown.id}`); } setOffset(offset = 0) { @@ -67,17 +61,14 @@ gl.FilteredSearchDropdownManager.addWordToInput(dataValue); } + // Return boolean based on whether it was set return dataValue !== null; } - dismissDropdown() { - this.input.focus(); - } - - dispatchInputEvent() { - // Propogate input change to FilteredSearchManager - // so that it can determine which dropdowns to open - this.input.dispatchEvent(new Event('input')); + renderContent(forceShowList = false) { + if (forceShowList && this.getCurrentHook().list.hidden) { + this.getCurrentHook().list.show(); + } } render(forceRenderContent = false, forceShowList = false) { @@ -88,11 +79,23 @@ if (firstTimeInitialized || forceRenderContent) { this.renderContent(forceShowList); - } else if(currentHook.list.list.id !== this.listId) { + } else if(currentHook.list.list.id !== this.dropdown.id) { this.renderContent(forceShowList); } } + dismissDropdown() { + // Focusing on the input will dismiss dropdown + // (default droplab functionality) + this.input.focus(); + } + + dispatchInputEvent() { + // Propogate input change to FilteredSearchDropdownManager + // so that it can determine which dropdowns to open + this.input.dispatchEvent(new Event('input')); + } + hideDropdown() { this.getCurrentHook().list.hide(); } @@ -100,9 +103,7 @@ resetFilters() { const hook = this.getCurrentHook(); const data = hook.list.data; - const results = data.map(function(o) { - o.droplab_hidden = false; - }); + const results = data.map(o => o.droplab_hidden = false); hook.list.render(results); } } diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 index 67a474985c0..a0764c275e5 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 @@ -5,6 +5,8 @@ this.tokenizer = gl.FilteredSearchTokenizer; this.filteredSearchInput = document.querySelector('.filtered-search'); + this.setupMapping(); + this.cleanupWrapper = this.cleanup.bind(this); document.addEventListener('page:fetch', this.cleanupWrapper); } @@ -52,21 +54,22 @@ } } - static addWordToInput(word, addSpace) { - const filteredSearchInput = document.querySelector('.filtered-search') - const filteredSearchValue = filteredSearchInput.value; - const hasExistingValue = filteredSearchValue.length !== 0; - const { lastToken } = gl.FilteredSearchTokenizer.processTokens(filteredSearchValue); + static addWordToInput(word, addSpace = false) { + const input = document.querySelector('.filtered-search') + const value = input.value; + const hasExistingValue = value.length !== 0; + const { lastToken } = gl.FilteredSearchTokenizer.processTokens(value); if (lastToken.hasOwnProperty('key')) { - console.log(lastToken); // Spaces inside the token means that the token value will be escaped by quotes const hasQuotes = lastToken.value.indexOf(' ') !== -1; + + // Add 2 length to account for the length of the front and back quotes const lengthToRemove = hasQuotes ? lastToken.value.length + 2 : lastToken.value.length; - filteredSearchInput.value = filteredSearchValue.slice(0, -1 * (lengthToRemove)); + input.value = value.slice(0, -1 * (lengthToRemove)); } - filteredSearchInput.value += hasExistingValue && addSpace ? ` ${word}` : word; + input.value += hasExistingValue && addSpace ? ` ${word}` : word; } updateCurrentDropdownOffset() { @@ -74,6 +77,10 @@ } updateDropdownOffset(key) { + if (!this.font) { + this.font = window.getComputedStyle(this.filteredSearchInput).font; + } + const filterIconPadding = 27; const offset = gl.text.getTextWidth(this.filteredSearchInput.value, this.font) + filterIconPadding; @@ -87,19 +94,20 @@ let forceShowList = false; if (!this.mapping[key].reference) { - var dl = this.droplab; + const dl = this.droplab; const defaultArguments = [null, dl, element, this.filteredSearchInput]; const glArguments = defaultArguments.concat(this.mapping[key].extraArguments || []); + // Passing glArguments to `new gl[glClass]()` this.mapping[key].reference = new (Function.prototype.bind.apply(gl[glClass], glArguments)); } if (firstLoad) { - this.mapping[key].reference.configure(); + this.mapping[key].reference.init(); } if (this.currentDropdown === 'hint') { - // Clicked from hint dropdown + // Force the dropdown to show if it was clicked from the hint dropdown forceShowList = true; } @@ -117,15 +125,12 @@ this.droplab = new DropLab(); } - if (!this.font) { - this.font = window.getComputedStyle(this.filteredSearchInput).font; - } - const match = gl.FilteredSearchTokenKeys.get().filter(value => value.key === dropdownName.toLowerCase())[0]; const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key && this.mapping.hasOwnProperty(match.key); const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint'; if (shouldOpenFilterDropdown || shouldOpenHintDropdown) { + // `hint` is not listed as a tokenKey (since it is not a real `filter`) const key = match && match.hasOwnProperty('key') ? match.key : 'hint'; this.load(key, firstLoad); } @@ -137,14 +142,12 @@ const { lastToken } = this.tokenizer.processTokens(this.filteredSearchInput.value); if (typeof lastToken === 'string') { - // Token is not fully initialized yet - // because it has no value + // Token is not fully initialized yet because it has no value // Eg. token = 'label:' const { tokenKey } = this.tokenizer.parseToken(lastToken); this.loadDropdown(tokenKey); } else if (lastToken.hasOwnProperty('key')) { - // Token has been initialized into an object - // because it has a value + // Token has been initialized into an object because it has a value this.loadDropdown(lastToken.key); } else { this.loadDropdown('hint'); diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index d9ea44b3a13..d3bccc4b14c 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -1,5 +1,6 @@ /* eslint-disable no-param-reassign */ ((global) => { + // TODO: Encapsulate inside class? function toggleClearSearchButton(e) { const clearSearchButton = document.querySelector('.clear-search'); @@ -25,6 +26,7 @@ let conditionIndex = 0; const validCondition = gl.FilteredSearchTokenKeys.get() .filter(v => v.conditions && v.conditions.filter((c, index) => { + // TODO: Add comment here if (c.url === p) { conditionIndex = index; } @@ -32,6 +34,7 @@ })[0])[0]; if (validCondition) { + // Parse params based on rules provided in the conditions key of gl.FilteredSearchTokenKeys.get() inputValue += `${validCondition.key}:${validCondition.conditions[conditionIndex].keyword}`; inputValue += ' '; } else { @@ -77,7 +80,6 @@ this.clearSearchButton = document.querySelector('.clear-search'); this.dropdownManager = new gl.FilteredSearchDropdownManager(); - this.dropdownManager.setupMapping(); this.bindEvents(); loadSearchParamsFromURL(); this.dropdownManager.setDropdown(); @@ -130,7 +132,6 @@ } checkForEnter(e) { - // Enter KeyCode if (e.keyCode === 13) { e.stopPropagation(); e.preventDefault(); @@ -143,7 +144,6 @@ } search() { - console.log('search'); let path = '?scope=all&utf8=✓'; // Check current state @@ -152,9 +152,10 @@ const defaultState = 'opened'; let currentState = defaultState; - const { tokens, searchToken } = this.tokenizer.processTokens(document.querySelector('.filtered-search').value); + const { tokens, searchToken } = this.tokenizer.processTokens(this.filteredSearchInput.value); if (stateIndex !== -1) { + // TODO: Add comment here const remaining = currentPath.slice(stateIndex + 6); const separatorIndex = remaining.indexOf('&'); diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 index 4abb5e94d81..ac45d3b7986 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 @@ -1,15 +1,6 @@ /* eslint-disable no-param-reassign */ ((global) => { class FilteredSearchTokenizer { - // TODO: Remove when going to pro - static printTokens(tokens, searchToken, lastToken) { - console.log('tokens:'); - tokens.forEach(token => console.log(token)); - console.log(`search: ${searchToken}`); - console.log('last token:'); - console.log(lastToken); - } - static parseToken(input) { const colonIndex = input.indexOf(':'); let tokenKey; @@ -163,9 +154,6 @@ searchToken = searchTerms.trim(); - // TODO: Remove when going to PRO - gl.FilteredSearchTokenizer.printTokens(tokens, searchToken, lastToken); - return { tokens, searchToken, -- cgit v1.2.1 From aebee11884d2176a45cb17efa97bf3fdbc95449a Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 10:55:43 -0600 Subject: Fix bug where labels with spaces weren't being escaped when selected --- app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 index 0969df65836..84abaa920d6 100644 --- a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 @@ -29,7 +29,6 @@ let escapedText = text; const hasSpace = text.indexOf(' ') !== -1; const hasDoubleQuote = text.indexOf('"') !== -1; - const hasSingleQuote = text.indexOf('\'') !== -1; // Encapsulate value with quotes if it has spaces // Known side effect: values's with both single and double quotes @@ -37,7 +36,8 @@ if (hasSpace) { if (hasDoubleQuote) { escapedText = `'${text}'`; - } else if (hasSingleQuote) { + } else { + // Encapsulate singleQuotes or if it hasSpace escapedText = `"${text}"`; } } -- cgit v1.2.1 From 262ad96aa9413e2ff1380930703e9e3a649bb855 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 10:55:55 -0600 Subject: Remove unnecessary function --- .../filtered_search/filtered_search_dropdown.js.es6 | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 index 85d684e3058..a9dbb0f7ccb 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -27,20 +27,12 @@ return this.droplab.hooks.filter(h => h.id === this.hookId)[0]; } - getSelectedText(selectedToken) { - // TODO: Get last word from FilteredSearchTokenizer - const lastWord = this.input.value.split(' ').last(); - const lastWordIndex = selectedToken.indexOf(lastWord); - - return lastWordIndex === -1 ? selectedToken : selectedToken.slice(lastWord.length); - } - itemClicked(e, getValueFunction) { const dataValueSet = this.setDataValueIfSelected(e.detail.selected); if (!dataValueSet) { - const value = getValueFunction(e.detail.selected) - gl.FilteredSearchDropdownManager.addWordToInput(this.getSelectedText(value)); + const value = getValueFunction(e.detail.selected); + gl.FilteredSearchDropdownManager.addWordToInput(value); } this.dismissDropdown(); -- cgit v1.2.1 From 274f3e23e35ddf3116cd7c227b94ce68378c76af Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 11:06:45 -0600 Subject: Add comments to resolve todos --- .../javascripts/filtered_search/filtered_search_manager.js.es6 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index d3bccc4b14c..14e2e698f93 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -26,7 +26,7 @@ let conditionIndex = 0; const validCondition = gl.FilteredSearchTokenKeys.get() .filter(v => v.conditions && v.conditions.filter((c, index) => { - // TODO: Add comment here + // Return TokenKeys that have conditions that much the URL if (c.url === p) { conditionIndex = index; } @@ -155,8 +155,8 @@ const { tokens, searchToken } = this.tokenizer.processTokens(this.filteredSearchInput.value); if (stateIndex !== -1) { - // TODO: Add comment here - const remaining = currentPath.slice(stateIndex + 6); + // Get currentState from url params if available + const remaining = currentPath.slice(stateIndex + 'state='.length); const separatorIndex = remaining.indexOf('&'); currentState = separatorIndex === -1 ? remaining : remaining.slice(0, separatorIndex); -- cgit v1.2.1 From 8925c9604f95b66a9b8a4579e321961312bfc78d Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 11:08:15 -0600 Subject: Add additional check before setting outerHTML --- app/assets/javascripts/droplab/droplab_ajax_filter.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/droplab/droplab_ajax_filter.js b/app/assets/javascripts/droplab/droplab_ajax_filter.js index 6e1eb080e3b..c6c062d0886 100644 --- a/app/assets/javascripts/droplab/droplab_ajax_filter.js +++ b/app/assets/javascripts/droplab/droplab_ajax_filter.js @@ -78,7 +78,11 @@ require('../window')(function(w){ this._loadUrlData(config.endpoint + this.buildParams(params)).then(function(data) { if (config.loadingTemplate && self.hook.list.data === undefined || self.hook.list.data.length === 0) { - self.hook.list.list.querySelector('[data-loading-template]').outerHTML = self.listTemplate; + const dataLoadingTemplate = self.hook.list.list.querySelector('[data-loading-template]'); + + if (dataLoadingTemplate) { + dataLoadingTemplate.outerHTML = self.listTemplate; + } } if (!self.destroyed) { -- cgit v1.2.1 From 16e3fe3f15971bc34c48d65c902ce83a156e350d Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 11:19:44 -0600 Subject: Fix missing method from refactoring --- app/assets/javascripts/filtered_search/dropdown_hint.js.es6 | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index 43a0b1da0fe..1aef27163c6 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -41,13 +41,20 @@ const tag = selected.querySelector('.js-filter-tag').innerText.trim(); if (tag.length) { - gl.FilteredSearchDropdownManager.addWordToInput(this.getSelectedText(token)); + gl.FilteredSearchDropdownManager.addWordToInput(this.getSelectedTextWithoutEscaping(token)); } this.dismissDropdown(); this.dispatchInputEvent(); } } + getSelectedTextWithoutEscaping(selectedToken) { + const lastWord = this.input.value.split(' ').last(); + const lastWordIndex = selectedToken.indexOf(lastWord); + + return lastWordIndex === -1 ? selectedToken : selectedToken.slice(lastWord.length); + } + renderContent() { this.droplab.changeHookList(this.hookId, this.dropdown, [droplabFilter], this.config); this.droplab.setData(this.hookId, dropdownData); -- cgit v1.2.1 From 6eafd748493e1125e1f5dea698dd3ca6affe15c5 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 16:25:31 -0600 Subject: Fix code styling issues --- .../javascripts/filtered_search/dropdown_hint.js.es6 | 2 +- .../filtered_search/filtered_search_dropdown.js.es6 | 9 +++++---- .../filtered_search_dropdown_manager.js.es6 | 20 ++++++++++---------- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index 1aef27163c6..a79779e4977 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -32,7 +32,7 @@ } itemClicked(e) { - const selected = e.detail.selected; + const { selected } = e.detail; if (selected.hasAttribute('data-value')) { this.dismissDropdown(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 index a9dbb0f7ccb..130e6bba341 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -24,14 +24,15 @@ } getCurrentHook() { - return this.droplab.hooks.filter(h => h.id === this.hookId)[0]; + return this.droplab.hooks.filter(h => h.id === this.hookId)[0] || null; } itemClicked(e, getValueFunction) { - const dataValueSet = this.setDataValueIfSelected(e.detail.selected); + const { selected } = e.detail; + const dataValueSet = this.setDataValueIfSelected(selected); if (!dataValueSet) { - const value = getValueFunction(e.detail.selected); + const value = getValueFunction(selected); gl.FilteredSearchDropdownManager.addWordToInput(value); } @@ -67,7 +68,7 @@ this.setAsDropdown(); const currentHook = this.getCurrentHook(); - const firstTimeInitialized = currentHook === undefined; + const firstTimeInitialized = currentHook === null; if (firstTimeInitialized || forceRenderContent) { this.renderContent(forceShowList); diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 index a0764c275e5..59166840c50 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 @@ -55,7 +55,7 @@ } static addWordToInput(word, addSpace = false) { - const input = document.querySelector('.filtered-search') + const input = document.querySelector('.filtered-search'); const value = input.value; const hasExistingValue = value.length !== 0; const { lastToken } = gl.FilteredSearchTokenizer.processTokens(value); @@ -88,22 +88,22 @@ } load(key, firstLoad = false) { - console.log(`🦄 load ${key} dropdown`); - const glClass = this.mapping[key].gl; - const element = this.mapping[key].element; + const mappingKey = this.mapping[key]; + const glClass = mappingKey.gl; + const element = mappingKey.element; let forceShowList = false; - if (!this.mapping[key].reference) { + if (!mappingKey.reference) { const dl = this.droplab; const defaultArguments = [null, dl, element, this.filteredSearchInput]; - const glArguments = defaultArguments.concat(this.mapping[key].extraArguments || []); + const glArguments = defaultArguments.concat(mappingKey.extraArguments || []); // Passing glArguments to `new gl[glClass]()` - this.mapping[key].reference = new (Function.prototype.bind.apply(gl[glClass], glArguments)); + mappingKey.reference = new (Function.prototype.bind.apply(gl[glClass], glArguments)); } if (firstLoad) { - this.mapping[key].reference.init(); + mappingKey.reference.init(); } if (this.currentDropdown === 'hint') { @@ -112,7 +112,7 @@ } this.updateDropdownOffset(key); - this.mapping[key].reference.render(firstLoad, forceShowList); + mappingKey.reference.render(firstLoad, forceShowList); this.currentDropdown = key; } @@ -120,7 +120,7 @@ loadDropdown(dropdownName = '') { let firstLoad = false; - if(!this.droplab) { + if (!this.droplab) { firstLoad = true; this.droplab = new DropLab(); } -- cgit v1.2.1 From d8b8b9c88d0fbe4cfa1fae0796feaa82136cc747 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 16:27:10 -0600 Subject: Add support for delete key --- app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 14e2e698f93..ebbd7e3129e 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -125,7 +125,9 @@ } checkForBackspace(e) { - if (e.keyCode === 8) { + // 8 = Backspace Key + // 46 = Delete Key + if (e.keyCode === 8 || e.keyCode === 46) { // Reposition dropdown so that it is aligned with cursor this.dropdownManager.updateCurrentDropdownOffset(); } -- cgit v1.2.1 From 214b6495ca1853c9653a0ff109e0163dbc1d1cb6 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 16:28:17 -0600 Subject: Remove unnecessary stopPropagation --- app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index ebbd7e3129e..e068b5d2ebf 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -115,7 +115,6 @@ } clearSearch(e) { - e.stopPropagation(); e.preventDefault(); this.filteredSearchInput.value = ''; @@ -135,7 +134,6 @@ checkForEnter(e) { if (e.keyCode === 13) { - e.stopPropagation(); e.preventDefault(); // Prevent droplab from opening dropdown -- cgit v1.2.1 From f0935c4da5bcd84fac51f7a0d61ee0c8d8181679 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 16:29:51 -0600 Subject: Fix regex for + --- app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index e068b5d2ebf..a89627384e9 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -40,7 +40,7 @@ } else { // Sanitize value since URL converts spaces into + // Replace before decode so that we know what was originally + versus the encoded + - const sanitizedValue = value ? decodeURIComponent(value.replace(/[+]/g, ' ')) : value; + const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value; const match = gl.FilteredSearchTokenKeys.get().filter(t => key === `${t.key}_${t.param}`)[0]; if (match) { -- cgit v1.2.1 From a30fbbddfb03a63305ff1bd273d7dd98c976936a Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 16:31:22 -0600 Subject: Reduce over-verboseness --- .../javascripts/filtered_search/filtered_search_manager.js.es6 | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index a89627384e9..77a9de96c8a 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -47,13 +47,11 @@ const sanitizedKey = key.slice(0, key.indexOf('_')); const valueHasSpace = sanitizedValue.indexOf(' ') !== -1; const symbol = match.symbol; - - const preferredQuotations = '"'; - let quotationsToUse = preferredQuotations; + let quotationsToUse; if (valueHasSpace) { // Prefer ", but use ' if required - quotationsToUse = sanitizedValue.indexOf(preferredQuotations) === -1 ? preferredQuotations : '\''; + quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\''; } inputValue += valueHasSpace ? `${sanitizedKey}:${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}` : `${sanitizedKey}:${symbol}${sanitizedValue}`; -- cgit v1.2.1 From 5116db243a2f1705462e792cbb71f666cfca98f0 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 16:34:01 -0600 Subject: Convert to single quotes --- app/assets/javascripts/lib/utils/text_utility.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index e47eccc3a33..db24bcf682b 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -27,8 +27,8 @@ * @see http://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393 */ // re-use canvas object for better performance - var canvas = gl.text.getTextWidth.canvas || (gl.text.getTextWidth.canvas = document.createElement("canvas")); - var context = canvas.getContext("2d"); + var canvas = gl.text.getTextWidth.canvas || (gl.text.getTextWidth.canvas = document.createElement('canvas')); + var context = canvas.getContext('2d'); context.font = font; var metrics = context.measureText(text); return metrics.width; -- cgit v1.2.1 From 78fe37b169602d898ffbd756189706559aad84f2 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 16:37:49 -0600 Subject: Move functions into class --- .../filtered_search/filtered_search_manager.js.es6 | 164 ++++++++++----------- 1 file changed, 81 insertions(+), 83 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 77a9de96c8a..00b7dc195bb 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -1,76 +1,5 @@ /* eslint-disable no-param-reassign */ ((global) => { - // TODO: Encapsulate inside class? - function toggleClearSearchButton(e) { - const clearSearchButton = document.querySelector('.clear-search'); - - if (e.target.value) { - clearSearchButton.classList.remove('hidden'); - } else { - clearSearchButton.classList.add('hidden'); - } - } - - function loadSearchParamsFromURL() { - // We can trust that each param has one & since values containing & will be encoded - // Remove the first character of search as it is always ? - const params = window.location.search.slice(1).split('&'); - let inputValue = ''; - - params.forEach((p) => { - const split = p.split('='); - const key = decodeURIComponent(split[0]); - const value = split[1]; - - // Check if it matches edge conditions listed in gl.FilteredSearchTokenKeys.get() - let conditionIndex = 0; - const validCondition = gl.FilteredSearchTokenKeys.get() - .filter(v => v.conditions && v.conditions.filter((c, index) => { - // Return TokenKeys that have conditions that much the URL - if (c.url === p) { - conditionIndex = index; - } - return c.url === p; - })[0])[0]; - - if (validCondition) { - // Parse params based on rules provided in the conditions key of gl.FilteredSearchTokenKeys.get() - inputValue += `${validCondition.key}:${validCondition.conditions[conditionIndex].keyword}`; - inputValue += ' '; - } else { - // Sanitize value since URL converts spaces into + - // Replace before decode so that we know what was originally + versus the encoded + - const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value; - const match = gl.FilteredSearchTokenKeys.get().filter(t => key === `${t.key}_${t.param}`)[0]; - - if (match) { - const sanitizedKey = key.slice(0, key.indexOf('_')); - const valueHasSpace = sanitizedValue.indexOf(' ') !== -1; - const symbol = match.symbol; - let quotationsToUse; - - if (valueHasSpace) { - // Prefer ", but use ' if required - quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\''; - } - - inputValue += valueHasSpace ? `${sanitizedKey}:${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}` : `${sanitizedKey}:${symbol}${sanitizedValue}`; - inputValue += ' '; - } else if (!match && key === 'search') { - inputValue += sanitizedValue; - inputValue += ' '; - } - } - }); - - // Trim the last space value - document.querySelector('.filtered-search').value = inputValue.trim(); - - if (inputValue.trim()) { - document.querySelector('.clear-search').classList.remove('hidden'); - } - } - class FilteredSearchManager { constructor() { this.tokenizer = gl.FilteredSearchTokenizer; @@ -79,7 +8,7 @@ this.dropdownManager = new gl.FilteredSearchDropdownManager(); this.bindEvents(); - loadSearchParamsFromURL(); + this.loadSearchParamsFromURL(); this.dropdownManager.setDropdown(); this.cleanupWrapper = this.cleanup.bind(this); @@ -93,12 +22,13 @@ bindEvents() { this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager); + this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this); this.checkForEnterWrapper = this.checkForEnter.bind(this); this.clearSearchWrapper = this.clearSearch.bind(this); this.checkForBackspaceWrapper = this.checkForBackspace.bind(this); this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper); - this.filteredSearchInput.addEventListener('input', toggleClearSearchButton); + this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper); this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper); this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper); this.clearSearchButton.addEventListener('click', this.clearSearchWrapper); @@ -106,21 +36,12 @@ unbindEvents() { this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper); - this.filteredSearchInput.removeEventListener('input', toggleClearSearchButton); + this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper); this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper); this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper); this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper); } - clearSearch(e) { - e.preventDefault(); - - this.filteredSearchInput.value = ''; - this.clearSearchButton.classList.add('hidden'); - - this.dropdownManager.resetDropdowns(); - } - checkForBackspace(e) { // 8 = Backspace Key // 46 = Delete Key @@ -141,6 +62,83 @@ } } + toggleClearSearchButton(e) { + if (e.target.value) { + this.clearSearchButton.classList.remove('hidden'); + } else { + this.clearSearchButton.classList.add('hidden'); + } + } + + clearSearch(e) { + e.preventDefault(); + + this.filteredSearchInput.value = ''; + this.clearSearchButton.classList.add('hidden'); + + this.dropdownManager.resetDropdowns(); + } + + loadSearchParamsFromURL() { + // We can trust that each param has one & since values containing & will be encoded + // Remove the first character of search as it is always ? + const params = window.location.search.slice(1).split('&'); + let inputValue = ''; + + params.forEach((p) => { + const split = p.split('='); + const key = decodeURIComponent(split[0]); + const value = split[1]; + + // Check if it matches edge conditions listed in gl.FilteredSearchTokenKeys.get() + let conditionIndex = 0; + const validCondition = gl.FilteredSearchTokenKeys.get() + .filter(v => v.conditions && v.conditions.filter((c, index) => { + // Return TokenKeys that have conditions that much the URL + if (c.url === p) { + conditionIndex = index; + } + return c.url === p; + })[0])[0]; + + if (validCondition) { + // Parse params based on rules provided in the conditions key of gl.FilteredSearchTokenKeys.get() + inputValue += `${validCondition.key}:${validCondition.conditions[conditionIndex].keyword}`; + inputValue += ' '; + } else { + // Sanitize value since URL converts spaces into + + // Replace before decode so that we know what was originally + versus the encoded + + const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value; + const match = gl.FilteredSearchTokenKeys.get().filter(t => key === `${t.key}_${t.param}`)[0]; + + if (match) { + const sanitizedKey = key.slice(0, key.indexOf('_')); + const valueHasSpace = sanitizedValue.indexOf(' ') !== -1; + const symbol = match.symbol; + let quotationsToUse; + + if (valueHasSpace) { + // Prefer ", but use ' if required + quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\''; + } + + inputValue += valueHasSpace ? `${sanitizedKey}:${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}` : `${sanitizedKey}:${symbol}${sanitizedValue}`; + inputValue += ' '; + } else if (!match && key === 'search') { + inputValue += sanitizedValue; + inputValue += ' '; + } + } + }); + + // Trim the last space value + this.filteredSearchInput.value = inputValue.trim(); + + if (inputValue.trim()) { + this.clearSearchButton.classList.remove('hidden'); + } + } + search() { let path = '?scope=all&utf8=✓'; -- cgit v1.2.1 From e9886b5704ae60a0e3517205de3958e9c0044a99 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 16:42:00 -0600 Subject: Convert string concatenations with an array join --- .../filtered_search/filtered_search_manager.js.es6 | 25 ++++++++++------------ 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 00b7dc195bb..d0e39b6390d 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -83,7 +83,7 @@ // We can trust that each param has one & since values containing & will be encoded // Remove the first character of search as it is always ? const params = window.location.search.slice(1).split('&'); - let inputValue = ''; + let inputValues = []; params.forEach((p) => { const split = p.split('='); @@ -103,8 +103,7 @@ if (validCondition) { // Parse params based on rules provided in the conditions key of gl.FilteredSearchTokenKeys.get() - inputValue += `${validCondition.key}:${validCondition.conditions[conditionIndex].keyword}`; - inputValue += ' '; + inputValues.push(`${validCondition.key}:${validCondition.conditions[conditionIndex].keyword}`); } else { // Sanitize value since URL converts spaces into + // Replace before decode so that we know what was originally + versus the encoded + @@ -122,25 +121,23 @@ quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\''; } - inputValue += valueHasSpace ? `${sanitizedKey}:${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}` : `${sanitizedKey}:${symbol}${sanitizedValue}`; - inputValue += ' '; + inputValues.push(valueHasSpace ? `${sanitizedKey}:${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}` : `${sanitizedKey}:${symbol}${sanitizedValue}`); } else if (!match && key === 'search') { - inputValue += sanitizedValue; - inputValue += ' '; + inputValues.push(sanitizedValue); } } }); // Trim the last space value - this.filteredSearchInput.value = inputValue.trim(); + this.filteredSearchInput.value = inputValues.join(' '); - if (inputValue.trim()) { + if (inputValues.length > 0) { this.clearSearchButton.classList.remove('hidden'); } } search() { - let path = '?scope=all&utf8=✓'; + let paths = []; // Check current state const currentPath = window.location.search; @@ -158,7 +155,7 @@ currentState = separatorIndex === -1 ? remaining : remaining.slice(0, separatorIndex); } - path += `&state=${currentState}`; + paths.push(`state=${currentState}`); tokens.forEach((token) => { const match = gl.FilteredSearchTokenKeys.get().filter(t => t.key === token.key)[0]; let tokenPath = ''; @@ -177,14 +174,14 @@ tokenPath = `${token.key}_${match.param}=${encodeURIComponent(token.value)}`; } - path += `&${tokenPath}`; + paths.push(tokenPath); }); if (searchToken) { - path += `&search=${encodeURIComponent(searchToken)}`; + paths.push(`search=${encodeURIComponent(searchToken)}`); } - window.location = path; + window.location = `?scope=all&utf8=✓&${paths.join('&')}`; } } -- cgit v1.2.1 From 98cb6101ec45e8758a9e85a3a24fcde9803ece18 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 21:07:42 -0600 Subject: Refactor static data to get information from other variables instead --- app/assets/javascripts/filtered_search/dropdown_user.js.es6 | 4 ++-- .../javascripts/filtered_search/filtered_search_dropdown.js.es6 | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 index 8bc274e0b12..69b1ec3ea04 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 @@ -15,7 +15,7 @@ project_id: this.getProjectId(), current_user: true, }, - searchValueFunction: this.getSearchInput, + searchValueFunction: this.getSearchInput.bind(this), loadingTemplate: this.loadingTemplate, }, }; @@ -37,7 +37,7 @@ } getSearchInput() { - const query = document.querySelector('.filtered-search').value; + const query = this.input.value; const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); const valueWithoutColon = value.slice(1); const hasPrefix = valueWithoutColon[0] === '@'; diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 index 130e6bba341..a5d8b0969c6 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -5,7 +5,7 @@ class FilteredSearchDropdown { constructor(droplab, dropdown, input) { this.droplab = droplab; - this.hookId = 'filtered-search'; + this.hookId = input.getAttribute('data-id'); this.input = input; this.dropdown = dropdown; this.loadingTemplate = `
-- cgit v1.2.1 From 091a3e66e9f2616eefca0f3aba090063116629e2 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 21:15:50 -0600 Subject: Add getParameterByName --- .../filtered_search/filtered_search_manager.js.es6 | 18 ++---------------- app/assets/javascripts/lib/utils/common_utils.js.es6 | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index d0e39b6390d..2237a21ca60 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -138,24 +138,10 @@ search() { let paths = []; - - // Check current state - const currentPath = window.location.search; - const stateIndex = currentPath.indexOf('state='); - const defaultState = 'opened'; - let currentState = defaultState; - const { tokens, searchToken } = this.tokenizer.processTokens(this.filteredSearchInput.value); - - if (stateIndex !== -1) { - // Get currentState from url params if available - const remaining = currentPath.slice(stateIndex + 'state='.length); - const separatorIndex = remaining.indexOf('&'); - - currentState = separatorIndex === -1 ? remaining : remaining.slice(0, separatorIndex); - } - + const currentState = gl.utils.getParameterByName('state') || 'opened'; paths.push(`state=${currentState}`); + tokens.forEach((token) => { const match = gl.FilteredSearchTokenKeys.get().filter(t => t.key === token.key)[0]; let tokenPath = ''; diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6 index b8d637a9827..f0186c1390f 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js.es6 +++ b/app/assets/javascripts/lib/utils/common_utils.js.es6 @@ -124,6 +124,22 @@ return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : '/' + parsedUrl.pathname; }; + gl.utils.getParameterByName = function(name) { + var url = window.location.href; + var param = name.replace(/[[\]]/g, '\\$&'); + var regex = new RegExp(`[?&]${param}(=([^&#]*)|&|#|$)`); + var results = regex.exec(url); + + if (!results) { + return null; + } + + if (!results[2]) { + return ''; + } + return decodeURIComponent(results[2].replace(/\+/g, ' ')); + }; + gl.utils.isMetaKey = function(e) { return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; }; -- cgit v1.2.1 From bcb00bdc487ad0d0e95c4a46a7d9437dcefc4e33 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 21:19:16 -0600 Subject: Use turbolinks instead of window.location --- app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 2237a21ca60..e087d0fd45b 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -167,7 +167,7 @@ paths.push(`search=${encodeURIComponent(searchToken)}`); } - window.location = `?scope=all&utf8=âś“&${paths.join('&')}`; + Turbolinks.visit(`?scope=all&utf8=âś“&${paths.join('&')}`); } } -- cgit v1.2.1 From 49231ccef2fb0bd0cd10d636864d1d50ea70cbdc Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 21:24:55 -0600 Subject: Refactor getUrlParamsArray() --- .../javascripts/filtered_search/filtered_search_manager.js.es6 | 4 +--- app/assets/javascripts/lib/utils/common_utils.js.es6 | 6 ++++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index e087d0fd45b..3e57215d608 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -80,9 +80,7 @@ } loadSearchParamsFromURL() { - // We can trust that each param has one & since values containing & will be encoded - // Remove the first character of search as it is always ? - const params = window.location.search.slice(1).split('&'); + const params = gl.utils.getUrlParamsArray(); let inputValues = []; params.forEach((p) => { diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6 index f0186c1390f..7a18f760e1b 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js.es6 +++ b/app/assets/javascripts/lib/utils/common_utils.js.es6 @@ -124,6 +124,12 @@ return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : '/' + parsedUrl.pathname; }; + gl.utils.getUrlParamsArray = function () { + // We can trust that each param has one & since values containing & will be encoded + // Remove the first character of search as it is always ? + return window.location.search.slice(1).split('&'); + } + gl.utils.getParameterByName = function(name) { var url = window.location.href; var param = name.replace(/[[\]]/g, '\\$&'); -- cgit v1.2.1 From bf16e91f2494912d44bc3a52d99ab36d3b33cd47 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 22:15:31 -0600 Subject: Refactor FilteredSearchTokenKeys model --- .../filtered_search_dropdown_manager.js.es6 | 2 +- .../filtered_search/filtered_search_manager.js.es6 | 48 ++++------- .../filtered_search_token_keys.js.es6 | 97 ++++++++++++++-------- .../filtered_search/filtered_search_tokenizer.es6 | 5 +- 4 files changed, 81 insertions(+), 71 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 index 59166840c50..682857d1899 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 @@ -125,7 +125,7 @@ this.droplab = new DropLab(); } - const match = gl.FilteredSearchTokenKeys.get().filter(value => value.key === dropdownName.toLowerCase())[0]; + const match = gl.FilteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase()); const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key && this.mapping.hasOwnProperty(match.key); const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint'; diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 3e57215d608..d7fb3a0c204 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -85,42 +85,32 @@ params.forEach((p) => { const split = p.split('='); - const key = decodeURIComponent(split[0]); + const keyParam = decodeURIComponent(split[0]); const value = split[1]; - // Check if it matches edge conditions listed in gl.FilteredSearchTokenKeys.get() - let conditionIndex = 0; - const validCondition = gl.FilteredSearchTokenKeys.get() - .filter(v => v.conditions && v.conditions.filter((c, index) => { - // Return TokenKeys that have conditions that much the URL - if (c.url === p) { - conditionIndex = index; - } - return c.url === p; - })[0])[0]; + // Check if it matches edge conditions listed in gl.FilteredSearchTokenKeys + const condition = gl.FilteredSearchTokenKeys.searchByConditionUrl(p); - if (validCondition) { - // Parse params based on rules provided in the conditions key of gl.FilteredSearchTokenKeys.get() - inputValues.push(`${validCondition.key}:${validCondition.conditions[conditionIndex].keyword}`); + if (condition) { + inputValues.push(`${condition.tokenKey}:${condition.value}`); } else { // Sanitize value since URL converts spaces into + // Replace before decode so that we know what was originally + versus the encoded + const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value; - const match = gl.FilteredSearchTokenKeys.get().filter(t => key === `${t.key}_${t.param}`)[0]; + const match = gl.FilteredSearchTokenKeys.searchByKeyParam(keyParam); if (match) { - const sanitizedKey = key.slice(0, key.indexOf('_')); - const valueHasSpace = sanitizedValue.indexOf(' ') !== -1; + const sanitizedKey = keyParam.slice(0, keyParam.indexOf('_')); const symbol = match.symbol; - let quotationsToUse; + let quotationsToUse = ''; - if (valueHasSpace) { + if (sanitizedValue.indexOf(' ') !== -1) { // Prefer ", but use ' if required quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\''; } - inputValues.push(valueHasSpace ? `${sanitizedKey}:${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}` : `${sanitizedKey}:${symbol}${sanitizedValue}`); - } else if (!match && key === 'search') { + inputValues.push(`${sanitizedKey}:${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`); + } else if (!match && keyParam === 'search') { inputValues.push(sanitizedValue); } } @@ -141,21 +131,17 @@ paths.push(`state=${currentState}`); tokens.forEach((token) => { - const match = gl.FilteredSearchTokenKeys.get().filter(t => t.key === token.key)[0]; + const condition = gl.FilteredSearchTokenKeys.searchByConditionKeyValue(token.key, token.value.toLowerCase()); + const { param } = gl.FilteredSearchTokenKeys.searchByKey(token.key); let tokenPath = ''; - if (token.wildcard && match.conditions) { - const condition = match.conditions - .filter(c => c.keyword === token.value.toLowerCase())[0]; - - if (condition) { - tokenPath = `${condition.url}`; - } + if (token.wildcard && condition) { + tokenPath = condition.url; } else if (!token.wildcard) { // Remove the wildcard token - tokenPath = `${token.key}_${match.param}=${encodeURIComponent(token.value.slice(1))}`; + tokenPath = `${token.key}_${param}=${encodeURIComponent(token.value.slice(1))}`; } else { - tokenPath = `${token.key}_${match.param}=${encodeURIComponent(token.value)}`; + tokenPath = `${token.key}_${param}=${encodeURIComponent(token.value)}`; } paths.push(tokenPath); diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 index 8d38a29a354..97eab6be8df 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 @@ -1,43 +1,68 @@ /* eslint-disable no-param-reassign */ ((global) => { + const tokenKeys = [{ + key: 'author', + type: 'string', + param: 'username', + symbol: '@', + }, { + key: 'assignee', + type: 'string', + param: 'username', + symbol: '@', + }, { + key: 'milestone', + type: 'string', + param: 'title', + symbol: '%', + }, { + key: 'label', + type: 'array', + param: 'name[]', + symbol: '~', + }]; + + const conditions = [{ + url: 'assignee_id=0', + tokenKey: 'assignee', + value: 'none', + }, { + url: 'milestone_title=No+Milestone', + tokenKey: 'milestone', + value: 'none', + }, { + url: 'milestone_title=%23upcoming', + tokenKey: 'milestone', + value: 'upcoming', + }, { + url: 'label_name[]=No+Label', + tokenKey: 'label', + value: 'none', + }]; + class FilteredSearchTokenKeys { static get() { - return [{ - key: 'author', - type: 'string', - param: 'username', - symbol: '@', - }, { - key: 'assignee', - type: 'string', - param: 'username', - symbol: '@', - conditions: [{ - keyword: 'none', - url: 'assignee_id=0', - }], - }, { - key: 'milestone', - type: 'string', - param: 'title', - symbol: '%', - conditions: [{ - keyword: 'none', - url: 'milestone_title=No+Milestone', - }, { - keyword: 'upcoming', - url: 'milestone_title=%23upcoming', - }], - }, { - key: 'label', - type: 'array', - param: 'name[]', - symbol: '~', - conditions: [{ - keyword: 'none', - url: 'label_name[]=No+Label', - }], - }]; + return tokenKeys; + } + + static searchByKey(key) { + return tokenKeys.find(tokenKey => tokenKey.key === key) || null; + } + + static searchBySymbol(symbol) { + return tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null; + } + + static searchByKeyParam(keyParam) { + return tokenKeys.find(tokenKey => keyParam === `${tokenKey.key}_${tokenKey.param}`) || null; + } + + static searchByConditionUrl(url) { + return conditions.find(condition => condition.url === url) || null; + } + + static searchByConditionKeyValue(key, value) { + return conditions.find(condition => condition.tokenKey === key && condition.value === value) || null; } } diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 index ac45d3b7986..365171252a1 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 @@ -73,7 +73,6 @@ let tokens = []; let searchToken = ''; let lastToken = ''; - const validTokenKeys = gl.FilteredSearchTokenKeys.get(); const inputs = input.split(' '); let searchTerms = ''; @@ -107,8 +106,8 @@ if (colonIndex !== -1) { const { tokenKey, tokenValue, tokenSymbol } = gl.FilteredSearchTokenizer.parseToken(i); - const keyMatch = validTokenKeys.filter(v => v.key === tokenKey)[0]; - const symbolMatch = validTokenKeys.filter(v => v.symbol === tokenSymbol)[0]; + const keyMatch = gl.FilteredSearchTokenKeys.searchByKey(tokenKey); + const symbolMatch = gl.FilteredSearchTokenKeys.searchBySymbol(tokenSymbol); const doubleQuoteOccurrences = tokenValue.split('"').length - 1; const singleQuoteOccurrences = tokenValue.split('\'').length - 1; -- cgit v1.2.1 From a26cc6b25c2602fd4a47808b8c9c48dea789c6bf Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 23:16:45 -0600 Subject: Simplify if else to make code easier to understand --- .../javascripts/filtered_search/filtered_search_manager.js.es6 | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index d7fb3a0c204..87bcbd272ca 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -137,11 +137,12 @@ if (token.wildcard && condition) { tokenPath = condition.url; - } else if (!token.wildcard) { - // Remove the wildcard token - tokenPath = `${token.key}_${param}=${encodeURIComponent(token.value.slice(1))}`; - } else { + } else if (token.wildcard) { + // wildcard means that the token does not have a symbol tokenPath = `${token.key}_${param}=${encodeURIComponent(token.value)}`; + } else { + // Remove the token symbol + tokenPath = `${token.key}_${param}=${encodeURIComponent(token.value.slice(1))}`; } paths.push(tokenPath); -- cgit v1.2.1 From 27b2204009fb8fee409df013de013146bad1bfde Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 13 Dec 2016 08:51:49 -0600 Subject: Convert hasOwnProperty check to if statement --- app/assets/javascripts/dispatcher.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 5a9ee5c7d78..9a76131b87f 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -84,7 +84,7 @@ break; case 'projects:merge_requests:index': case 'projects:issues:index': - if(gl.hasOwnProperty('FilteredSearchManager')) { + if(gl.FilteredSearchManager) { new gl.FilteredSearchManager(); } Issuable.init(); -- cgit v1.2.1 From 6700b76bec2a3b6564bd9da12b580b998d767d30 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 13 Dec 2016 08:52:43 -0600 Subject: Fix eslint --- .../filtered_search/dropdown_hint.js.es6 | 29 ++++++++++---------- .../filtered_search/dropdown_non_user.js.es6 | 31 ++++++++++++---------- .../filtered_search/dropdown_user.js.es6 | 14 +++++----- .../filtered_search_dropdown.js.es6 | 16 ++++++----- .../filtered_search_dropdown_manager.js.es6 | 24 +++++++++-------- .../filtered_search/filtered_search_manager.js.es6 | 15 ++++++----- .../filtered_search_token_keys.js.es6 | 11 ++++---- .../filtered_search/filtered_search_tokenizer.es6 | 8 +++--- .../javascripts/lib/utils/common_utils.js.es6 | 2 +- app/assets/javascripts/lib/utils/text_utility.js | 3 +-- 10 files changed, 81 insertions(+), 72 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index a79779e4977..b920b17d915 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -1,20 +1,18 @@ -/* eslint-disable no-param-reassign */ /*= require filtered_search/filtered_search_dropdown */ - -((global) => { +(() => { const dropdownData = [{ icon: 'fa-pencil', hint: 'author:', - tag: '<author>' - },{ + tag: '<author>', + }, { icon: 'fa-user', hint: 'assignee:', tag: '<assignee>', - },{ + }, { icon: 'fa-clock-o', hint: 'milestone:', tag: '<milestone>', - },{ + }, { icon: 'fa-tag', hint: 'label:', tag: '<label>', @@ -27,7 +25,7 @@ droplabFilter: { template: 'hint', filterFunction: this.filterMethod, - } + }, }; } @@ -41,7 +39,8 @@ const tag = selected.querySelector('.js-filter-tag').innerText.trim(); if (tag.length) { - gl.FilteredSearchDropdownManager.addWordToInput(this.getSelectedTextWithoutEscaping(token)); + gl.FilteredSearchDropdownManager + .addWordToInput(this.getSelectedTextWithoutEscaping(token)); } this.dismissDropdown(); this.dispatchInputEvent(); @@ -61,15 +60,16 @@ } filterMethod(item, query) { + const updatedItem = item; const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); if (value === '') { - item.droplab_hidden = false; + updatedItem.droplab_hidden = false; } else { - item.droplab_hidden = item['hint'].indexOf(value) === -1; + updatedItem.droplab_hidden = updatedItem.hint.indexOf(value) === -1; } - return item; + return updatedItem; } init() { @@ -77,5 +77,6 @@ } } - global.DropdownHint = DropdownHint; -})(window.gl || (window.gl = {})); + window.gl = window.gl || {}; + gl.DropdownHint = DropdownHint; +})(); diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 index 84abaa920d6..95133db4c04 100644 --- a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 @@ -1,26 +1,24 @@ -/* eslint-disable no-param-reassign */ /*= require filtered_search/filtered_search_dropdown */ - -((global) => { +(() => { class DropdownNonUser extends gl.FilteredSearchDropdown { constructor(droplab, dropdown, input, endpoint, symbol) { super(droplab, dropdown, input); this.symbol = symbol; this.config = { droplabAjax: { - endpoint: endpoint, + endpoint, method: 'setData', loadingTemplate: this.loadingTemplate, }, droplabFilter: { filterFunction: this.filterWithSymbol.bind(this, this.symbol), - } + }, }; } itemClicked(e) { super.itemClicked(e, (selected) => { - const title = e.detail.selected.querySelector('.js-data-value').innerText.trim(); + const title = selected.querySelector('.js-data-value').innerText.trim(); return `${this.symbol}${this.getEscapedText(title)}`; }); } @@ -46,30 +44,35 @@ } filterWithSymbol(filterSymbol, item, query) { + const updatedItem = item; const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); const valueWithoutColon = value.slice(1).toLowerCase(); const prefix = valueWithoutColon[0]; const valueWithoutPrefix = valueWithoutColon.slice(1); - const title = item.title.toLowerCase(); + const title = updatedItem.title.toLowerCase(); // Eg. filterSymbol = ~ for labels - const matchWithoutPrefix = prefix === filterSymbol && title.indexOf(valueWithoutPrefix) !== -1; + const matchWithoutPrefix = + prefix === filterSymbol && title.indexOf(valueWithoutPrefix) !== -1; const match = title.indexOf(valueWithoutColon) !== -1; - item.droplab_hidden = !match && !matchWithoutPrefix; - return item; + updatedItem.droplab_hidden = !match && !matchWithoutPrefix; + return updatedItem; } renderContent(forceShowList = false) { - this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjax, droplabFilter], this.config); + this.droplab + .changeHookList(this.hookId, this.dropdown, [droplabAjax, droplabFilter], this.config); super.renderContent(forceShowList); } init() { - this.droplab.addHook(this.input, this.dropdown, [droplabAjax, droplabFilter], this.config).init(); + this.droplab + .addHook(this.input, this.dropdown, [droplabAjax, droplabFilter], this.config).init(); } } - global.DropdownNonUser = DropdownNonUser; -})(window.gl || (window.gl = {})); + window.gl = window.gl || {}; + gl.DropdownNonUser = DropdownNonUser; +})(); diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 index 69b1ec3ea04..2ee46559e63 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 @@ -1,7 +1,5 @@ -/* eslint-disable no-param-reassign */ /*= require filtered_search/filtered_search_dropdown */ - -((global) => { +(() => { class DropdownUser extends gl.FilteredSearchDropdown { constructor(droplab, dropdown, input) { super(droplab, dropdown, input); @@ -22,9 +20,8 @@ } itemClicked(e) { - super.itemClicked(e, (selected) => { - return selected.querySelector('.dropdown-light-content').innerText.trim(); - }); + super.itemClicked(e, + selected => selected.querySelector('.dropdown-light-content').innerText.trim()); } renderContent(forceShowList = false) { @@ -51,5 +48,6 @@ } } - global.DropdownUser = DropdownUser; -})(window.gl || (window.gl = {})); + window.gl = window.gl || {}; + gl.DropdownUser = DropdownUser; +})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 index a5d8b0969c6..7ddfdca10fa 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -1,5 +1,4 @@ -/* eslint-disable no-param-reassign */ -((global) => { +(() => { const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger'; class FilteredSearchDropdown { @@ -72,7 +71,7 @@ if (firstTimeInitialized || forceRenderContent) { this.renderContent(forceShowList); - } else if(currentHook.list.list.id !== this.dropdown.id) { + } else if (currentHook.list.list.id !== this.dropdown.id) { this.renderContent(forceShowList); } } @@ -96,10 +95,15 @@ resetFilters() { const hook = this.getCurrentHook(); const data = hook.list.data; - const results = data.map(o => o.droplab_hidden = false); + const results = data.map((o) => { + const updated = o; + updated.droplab_hidden = false; + return updated; + }); hook.list.render(results); } } - global.FilteredSearchDropdown = FilteredSearchDropdown; -})(window.gl || (window.gl = {})); + window.gl = window.gl || {}; + gl.FilteredSearchDropdown = FilteredSearchDropdown; +})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 index 682857d1899..7864ebf7aa1 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 @@ -1,5 +1,4 @@ -/* eslint-disable no-param-reassign */ -((global) => { +(() => { class FilteredSearchDropdownManager { constructor() { this.tokenizer = gl.FilteredSearchTokenizer; @@ -51,7 +50,7 @@ gl: 'DropdownHint', element: document.querySelector('#js-dropdown-hint'), }, - } + }; } static addWordToInput(word, addSpace = false) { @@ -60,7 +59,7 @@ const hasExistingValue = value.length !== 0; const { lastToken } = gl.FilteredSearchTokenizer.processTokens(value); - if (lastToken.hasOwnProperty('key')) { + if ({}.hasOwnProperty.call(lastToken, 'key')) { // Spaces inside the token means that the token value will be escaped by quotes const hasQuotes = lastToken.value.indexOf(' ') !== -1; @@ -82,7 +81,8 @@ } const filterIconPadding = 27; - const offset = gl.text.getTextWidth(this.filteredSearchInput.value, this.font) + filterIconPadding; + const offset = gl.text + .getTextWidth(this.filteredSearchInput.value, this.font) + filterIconPadding; this.mapping[key].reference.setOffset(offset); } @@ -99,7 +99,7 @@ const glArguments = defaultArguments.concat(mappingKey.extraArguments || []); // Passing glArguments to `new gl[glClass]()` - mappingKey.reference = new (Function.prototype.bind.apply(gl[glClass], glArguments)); + mappingKey.reference = new (Function.prototype.bind.apply(gl[glClass], glArguments))(); } if (firstLoad) { @@ -126,12 +126,13 @@ } const match = gl.FilteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase()); - const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key && this.mapping.hasOwnProperty(match.key); + const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key + && {}.hasOwnProperty.call(this.mapping, match.key); const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint'; if (shouldOpenFilterDropdown || shouldOpenHintDropdown) { // `hint` is not listed as a tokenKey (since it is not a real `filter`) - const key = match && match.hasOwnProperty('key') ? match.key : 'hint'; + const key = match && {}.hasOwnProperty.call(match, 'key') ? match.key : 'hint'; this.load(key, firstLoad); } @@ -146,7 +147,7 @@ // Eg. token = 'label:' const { tokenKey } = this.tokenizer.parseToken(lastToken); this.loadDropdown(tokenKey); - } else if (lastToken.hasOwnProperty('key')) { + } else if ({}.hasOwnProperty.call(lastToken, 'key')) { // Token has been initialized into an object because it has a value this.loadDropdown(lastToken.key); } else { @@ -173,5 +174,6 @@ } } - global.FilteredSearchDropdownManager = FilteredSearchDropdownManager; -})(window.gl || (window.gl = {})); + window.gl = window.gl || {}; + gl.FilteredSearchDropdownManager = FilteredSearchDropdownManager; +})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 87bcbd272ca..96131a673ef 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -1,5 +1,4 @@ -/* eslint-disable no-param-reassign */ -((global) => { +(() => { class FilteredSearchManager { constructor() { this.tokenizer = gl.FilteredSearchTokenizer; @@ -81,7 +80,7 @@ loadSearchParamsFromURL() { const params = gl.utils.getUrlParamsArray(); - let inputValues = []; + const inputValues = []; params.forEach((p) => { const split = p.split('='); @@ -125,13 +124,14 @@ } search() { - let paths = []; + const paths = []; const { tokens, searchToken } = this.tokenizer.processTokens(this.filteredSearchInput.value); const currentState = gl.utils.getParameterByName('state') || 'opened'; paths.push(`state=${currentState}`); tokens.forEach((token) => { - const condition = gl.FilteredSearchTokenKeys.searchByConditionKeyValue(token.key, token.value.toLowerCase()); + const condition = gl.FilteredSearchTokenKeys + .searchByConditionKeyValue(token.key, token.value.toLowerCase()); const { param } = gl.FilteredSearchTokenKeys.searchByKey(token.key); let tokenPath = ''; @@ -156,5 +156,6 @@ } } - global.FilteredSearchManager = FilteredSearchManager; -})(window.gl || (window.gl = {})); + window.gl = window.gl || {}; + gl.FilteredSearchManager = FilteredSearchManager; +})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 index 97eab6be8df..a1830d13e5f 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 @@ -1,5 +1,4 @@ -/* eslint-disable no-param-reassign */ -((global) => { +(() => { const tokenKeys = [{ key: 'author', type: 'string', @@ -62,9 +61,11 @@ } static searchByConditionKeyValue(key, value) { - return conditions.find(condition => condition.tokenKey === key && condition.value === value) || null; + return conditions + .find(condition => condition.tokenKey === key && condition.value === value) || null; } } - global.FilteredSearchTokenKeys = FilteredSearchTokenKeys; -})(window.gl || (window.gl = {})); + window.gl = window.gl || {}; + gl.FilteredSearchTokenKeys = FilteredSearchTokenKeys; +})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 index 365171252a1..0507f7bbc48 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 @@ -1,5 +1,4 @@ -/* eslint-disable no-param-reassign */ -((global) => { +(() => { class FilteredSearchTokenizer { static parseToken(input) { const colonIndex = input.indexOf(':'); @@ -161,5 +160,6 @@ } } - global.FilteredSearchTokenizer = FilteredSearchTokenizer; -})(window.gl || (window.gl = {})); + window.gl = window.gl || {}; + gl.FilteredSearchTokenizer = FilteredSearchTokenizer; +})(); diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6 index 7a18f760e1b..9f1a62bf8b1 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js.es6 +++ b/app/assets/javascripts/lib/utils/common_utils.js.es6 @@ -128,7 +128,7 @@ // We can trust that each param has one & since values containing & will be encoded // Remove the first character of search as it is always ? return window.location.search.slice(1).split('&'); - } + }; gl.utils.getParameterByName = function(name) { var url = window.location.href; diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index db24bcf682b..c856a26ae40 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -30,8 +30,7 @@ var canvas = gl.text.getTextWidth.canvas || (gl.text.getTextWidth.canvas = document.createElement('canvas')); var context = canvas.getContext('2d'); context.font = font; - var metrics = context.measureText(text); - return metrics.width; + return context.measureText(text).width; }; gl.text.selectedText = function(text, textarea) { return text.substring(textarea.selectionStart, textarea.selectionEnd); -- cgit v1.2.1 From 3d18319e0deae15836e994088f1254b28015d188 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 13 Dec 2016 09:52:23 -0600 Subject: Rename to .js.es6 --- .../filtered_search/filtered_search_tokenizer.es6 | 165 --------------------- .../filtered_search_tokenizer.js.es6 | 165 +++++++++++++++++++++ 2 files changed, 165 insertions(+), 165 deletions(-) delete mode 100644 app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 create mode 100644 app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 deleted file mode 100644 index 0507f7bbc48..00000000000 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 +++ /dev/null @@ -1,165 +0,0 @@ -(() => { - class FilteredSearchTokenizer { - static parseToken(input) { - const colonIndex = input.indexOf(':'); - let tokenKey; - let tokenValue; - let tokenSymbol; - - if (colonIndex !== -1) { - tokenKey = input.slice(0, colonIndex).toLowerCase(); - tokenValue = input.slice(colonIndex + 1); - tokenSymbol = tokenValue[0]; - } - - return { - tokenKey, - tokenValue, - tokenSymbol, - } - } - - static getLastTokenObject(input) { - const token = FilteredSearchTokenizer.getLastToken(input); - const colonIndex = token.indexOf(':'); - - const key = colonIndex !== -1 ? token.slice(0, colonIndex) : ''; - const value = colonIndex !== -1 ? token.slice(colonIndex) : token; - - return { - key, - value, - } - } - - static getLastToken(input) { - let completeToken = false; - let completeQuotation = true; - let lastQuotation = ''; - let i = input.length; - - const doubleQuote = '"'; - const singleQuote = '\''; - while(!completeToken && i >= 0) { - const isDoubleQuote = input[i] === doubleQuote; - const isSingleQuote = input[i] === singleQuote; - - // If the second quotation is found - if ((lastQuotation === doubleQuote && input[i] === doubleQuote) || - (lastQuotation === singleQuote && input[i] === singleQuote)) { - completeQuotation = true; - } - - // Save the first quotation - if ((input[i] === doubleQuote && lastQuotation === '') || - (input[i] === singleQuote && lastQuotation === '')) { - lastQuotation = input[i]; - completeQuotation = false; - } - - if (completeQuotation && input[i] === ' ') { - completeToken = true; - } else { - i--; - } - } - - // Adjust by 1 because of empty space - return input.slice(i + 1); - } - - static processTokens(input) { - let tokens = []; - let searchToken = ''; - let lastToken = ''; - - const inputs = input.split(' '); - let searchTerms = ''; - let lastQuotation = ''; - let incompleteToken = false; - - // Iterate through each word (broken up by spaces) - inputs.forEach((i) => { - if (incompleteToken) { - // Continue previous token as it had an escaped - // quote in the beginning - const prevToken = tokens.last(); - prevToken.value += ` ${i}`; - - // Remove last quotation from the value - const lastQuotationRegex = new RegExp(lastQuotation, 'g'); - prevToken.value = prevToken.value.replace(lastQuotationRegex, ''); - tokens[tokens.length - 1] = prevToken; - - // Check to see if this quotation completes the token value - if (i.indexOf(lastQuotation) !== -1) { - lastToken = tokens.last(); - incompleteToken = !incompleteToken; - } - - return; - } - - const colonIndex = i.indexOf(':'); - - if (colonIndex !== -1) { - const { tokenKey, tokenValue, tokenSymbol } = gl.FilteredSearchTokenizer.parseToken(i); - - const keyMatch = gl.FilteredSearchTokenKeys.searchByKey(tokenKey); - const symbolMatch = gl.FilteredSearchTokenKeys.searchBySymbol(tokenSymbol); - - const doubleQuoteOccurrences = tokenValue.split('"').length - 1; - const singleQuoteOccurrences = tokenValue.split('\'').length - 1; - - const doubleQuoteIndex = tokenValue.indexOf('"'); - const singleQuoteIndex = tokenValue.indexOf('\''); - - const doubleQuoteExist = doubleQuoteIndex !== -1; - const singleQuoteExist = singleQuoteIndex !== -1; - - const doubleQuoteExistOnly = doubleQuoteExist && !singleQuoteExist; - const doubleQuoteIsBeforeSingleQuote = doubleQuoteExist && singleQuoteExist && doubleQuoteIndex < singleQuoteIndex; - - const singleQuoteExistOnly = singleQuoteExist && !doubleQuoteExist; - const singleQuoteIsBeforeDoubleQuote = doubleQuoteExist && singleQuoteExist && singleQuoteIndex < doubleQuoteIndex; - - if ((doubleQuoteExistOnly || doubleQuoteIsBeforeSingleQuote) && doubleQuoteOccurrences % 2 !== 0) { - // " is found and is in front of ' (if any) - lastQuotation = '"'; - incompleteToken = true; - } else if ((singleQuoteExistOnly || singleQuoteIsBeforeDoubleQuote) && singleQuoteOccurrences % 2 !== 0) { - // ' is found and is in front of " (if any) - lastQuotation = '\''; - incompleteToken = true; - } - - if (keyMatch && tokenValue.length > 0) { - tokens.push({ - key: keyMatch.key, - value: tokenValue, - wildcard: symbolMatch ? false : true, - }); - lastToken = tokens.last(); - - return; - } - } - - // Add space for next term - searchTerms += `${i} `; - lastToken = i; - }, this); - - searchToken = searchTerms.trim(); - - return { - tokens, - searchToken, - lastToken, - }; - } - } - - window.gl = window.gl || {}; - gl.FilteredSearchTokenizer = FilteredSearchTokenizer; -})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 new file mode 100644 index 00000000000..0507f7bbc48 --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 @@ -0,0 +1,165 @@ +(() => { + class FilteredSearchTokenizer { + static parseToken(input) { + const colonIndex = input.indexOf(':'); + let tokenKey; + let tokenValue; + let tokenSymbol; + + if (colonIndex !== -1) { + tokenKey = input.slice(0, colonIndex).toLowerCase(); + tokenValue = input.slice(colonIndex + 1); + tokenSymbol = tokenValue[0]; + } + + return { + tokenKey, + tokenValue, + tokenSymbol, + } + } + + static getLastTokenObject(input) { + const token = FilteredSearchTokenizer.getLastToken(input); + const colonIndex = token.indexOf(':'); + + const key = colonIndex !== -1 ? token.slice(0, colonIndex) : ''; + const value = colonIndex !== -1 ? token.slice(colonIndex) : token; + + return { + key, + value, + } + } + + static getLastToken(input) { + let completeToken = false; + let completeQuotation = true; + let lastQuotation = ''; + let i = input.length; + + const doubleQuote = '"'; + const singleQuote = '\''; + while(!completeToken && i >= 0) { + const isDoubleQuote = input[i] === doubleQuote; + const isSingleQuote = input[i] === singleQuote; + + // If the second quotation is found + if ((lastQuotation === doubleQuote && input[i] === doubleQuote) || + (lastQuotation === singleQuote && input[i] === singleQuote)) { + completeQuotation = true; + } + + // Save the first quotation + if ((input[i] === doubleQuote && lastQuotation === '') || + (input[i] === singleQuote && lastQuotation === '')) { + lastQuotation = input[i]; + completeQuotation = false; + } + + if (completeQuotation && input[i] === ' ') { + completeToken = true; + } else { + i--; + } + } + + // Adjust by 1 because of empty space + return input.slice(i + 1); + } + + static processTokens(input) { + let tokens = []; + let searchToken = ''; + let lastToken = ''; + + const inputs = input.split(' '); + let searchTerms = ''; + let lastQuotation = ''; + let incompleteToken = false; + + // Iterate through each word (broken up by spaces) + inputs.forEach((i) => { + if (incompleteToken) { + // Continue previous token as it had an escaped + // quote in the beginning + const prevToken = tokens.last(); + prevToken.value += ` ${i}`; + + // Remove last quotation from the value + const lastQuotationRegex = new RegExp(lastQuotation, 'g'); + prevToken.value = prevToken.value.replace(lastQuotationRegex, ''); + tokens[tokens.length - 1] = prevToken; + + // Check to see if this quotation completes the token value + if (i.indexOf(lastQuotation) !== -1) { + lastToken = tokens.last(); + incompleteToken = !incompleteToken; + } + + return; + } + + const colonIndex = i.indexOf(':'); + + if (colonIndex !== -1) { + const { tokenKey, tokenValue, tokenSymbol } = gl.FilteredSearchTokenizer.parseToken(i); + + const keyMatch = gl.FilteredSearchTokenKeys.searchByKey(tokenKey); + const symbolMatch = gl.FilteredSearchTokenKeys.searchBySymbol(tokenSymbol); + + const doubleQuoteOccurrences = tokenValue.split('"').length - 1; + const singleQuoteOccurrences = tokenValue.split('\'').length - 1; + + const doubleQuoteIndex = tokenValue.indexOf('"'); + const singleQuoteIndex = tokenValue.indexOf('\''); + + const doubleQuoteExist = doubleQuoteIndex !== -1; + const singleQuoteExist = singleQuoteIndex !== -1; + + const doubleQuoteExistOnly = doubleQuoteExist && !singleQuoteExist; + const doubleQuoteIsBeforeSingleQuote = doubleQuoteExist && singleQuoteExist && doubleQuoteIndex < singleQuoteIndex; + + const singleQuoteExistOnly = singleQuoteExist && !doubleQuoteExist; + const singleQuoteIsBeforeDoubleQuote = doubleQuoteExist && singleQuoteExist && singleQuoteIndex < doubleQuoteIndex; + + if ((doubleQuoteExistOnly || doubleQuoteIsBeforeSingleQuote) && doubleQuoteOccurrences % 2 !== 0) { + // " is found and is in front of ' (if any) + lastQuotation = '"'; + incompleteToken = true; + } else if ((singleQuoteExistOnly || singleQuoteIsBeforeDoubleQuote) && singleQuoteOccurrences % 2 !== 0) { + // ' is found and is in front of " (if any) + lastQuotation = '\''; + incompleteToken = true; + } + + if (keyMatch && tokenValue.length > 0) { + tokens.push({ + key: keyMatch.key, + value: tokenValue, + wildcard: symbolMatch ? false : true, + }); + lastToken = tokens.last(); + + return; + } + } + + // Add space for next term + searchTerms += `${i} `; + lastToken = i; + }, this); + + searchToken = searchTerms.trim(); + + return { + tokens, + searchToken, + lastToken, + }; + } + } + + window.gl = window.gl || {}; + gl.FilteredSearchTokenizer = FilteredSearchTokenizer; +})(); -- cgit v1.2.1 From 4786a9780337839844d5839fefda51430e13685e Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 13 Dec 2016 21:36:54 -0600 Subject: Fix es6 errors --- app/assets/javascripts/droplab/droplab.js | 101 ++++++++++++++------- .../javascripts/droplab/droplab_ajax_filter.js | 2 +- .../javascripts/lib/utils/common_utils.js.es6 | 2 +- 3 files changed, 68 insertions(+), 37 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js index 359cd82bbcd..94236153e41 100644 --- a/app/assets/javascripts/droplab/droplab.js +++ b/app/assets/javascripts/droplab/droplab.js @@ -1,3 +1,29 @@ +// Determine where to place this +if (typeof Object.assign != 'function') { + Object.assign = function (target, varArgs) { // .length of function is 2 + 'use strict'; + if (target == null) { // TypeError if undefined or null + throw new TypeError('Cannot convert undefined or null to object'); + } + + var to = Object(target); + + for (var index = 1; index < arguments.length; index++) { + var nextSource = arguments[index]; + + if (nextSource != null) { // Skip over if undefined or null + for (var nextKey in nextSource) { + // Avoid bugs when hasOwnProperty is shadowed + if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { + to[nextKey] = nextSource[nextKey]; + } + } + } + } + return to; + }; +} + /* eslint-disable */ (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.droplab = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o Date: Tue, 13 Dec 2016 21:55:25 -0600 Subject: Fix eslint --- app/assets/javascripts/droplab/droplab.js | 2 +- .../filtered_search/dropdown_hint.js.es6 | 18 ++------- .../filtered_search/dropdown_non_user.js.es6 | 46 +++------------------- .../filtered_search/dropdown_user.js.es6 | 3 ++ .../filtered_search_dropdown.js.es6 | 13 +----- .../filtered_search_dropdown_manager.js.es6 | 2 + .../filtered_search/filtered_search_manager.js.es6 | 2 + .../filtered_search_tokenizer.js.es6 | 32 ++++++++------- 8 files changed, 37 insertions(+), 81 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js index 94236153e41..ed545ec8748 100644 --- a/app/assets/javascripts/droplab/droplab.js +++ b/app/assets/javascripts/droplab/droplab.js @@ -1,3 +1,4 @@ +/* eslint-disable */ // Determine where to place this if (typeof Object.assign != 'function') { Object.assign = function (target, varArgs) { // .length of function is 2 @@ -24,7 +25,6 @@ if (typeof Object.assign != 'function') { }; } -/* eslint-disable */ (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.droplab = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o { const dropdownData = [{ icon: 'fa-pencil', @@ -24,7 +27,7 @@ this.config = { droplabFilter: { template: 'hint', - filterFunction: this.filterMethod, + filterFunction: gl.DropdownUtils.filterMethod, }, }; } @@ -59,19 +62,6 @@ this.droplab.setData(this.hookId, dropdownData); } - filterMethod(item, query) { - const updatedItem = item; - const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); - - if (value === '') { - updatedItem.droplab_hidden = false; - } else { - updatedItem.droplab_hidden = updatedItem.hint.indexOf(value) === -1; - } - - return updatedItem; - } - init() { this.droplab.addHook(this.input, this.dropdown, [droplabFilter], this.config).init(); } diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 index 95133db4c04..54090375c5c 100644 --- a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 @@ -1,4 +1,8 @@ /*= require filtered_search/filtered_search_dropdown */ + +/* global droplabAjax */ +/* global droplabFilter */ + (() => { class DropdownNonUser extends gl.FilteredSearchDropdown { constructor(droplab, dropdown, input, endpoint, symbol) { @@ -11,7 +15,7 @@ loadingTemplate: this.loadingTemplate, }, droplabFilter: { - filterFunction: this.filterWithSymbol.bind(this, this.symbol), + filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol), }, }; } @@ -19,48 +23,10 @@ itemClicked(e) { super.itemClicked(e, (selected) => { const title = selected.querySelector('.js-data-value').innerText.trim(); - return `${this.symbol}${this.getEscapedText(title)}`; + return `${this.symbol}${gl.DropdownUtils.getEscapedText(title)}`; }); } - getEscapedText(text) { - let escapedText = text; - const hasSpace = text.indexOf(' ') !== -1; - const hasDoubleQuote = text.indexOf('"') !== -1; - - // Encapsulate value with quotes if it has spaces - // Known side effect: values's with both single and double quotes - // won't escape properly - if (hasSpace) { - if (hasDoubleQuote) { - escapedText = `'${text}'`; - } else { - // Encapsulate singleQuotes or if it hasSpace - escapedText = `"${text}"`; - } - } - - return escapedText; - } - - filterWithSymbol(filterSymbol, item, query) { - const updatedItem = item; - const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); - const valueWithoutColon = value.slice(1).toLowerCase(); - const prefix = valueWithoutColon[0]; - const valueWithoutPrefix = valueWithoutColon.slice(1); - - const title = updatedItem.title.toLowerCase(); - - // Eg. filterSymbol = ~ for labels - const matchWithoutPrefix = - prefix === filterSymbol && title.indexOf(valueWithoutPrefix) !== -1; - const match = title.indexOf(valueWithoutColon) !== -1; - - updatedItem.droplab_hidden = !match && !matchWithoutPrefix; - return updatedItem; - } - renderContent(forceShowList = false) { this.droplab .changeHookList(this.hookId, this.dropdown, [droplabAjax, droplabFilter], this.config); diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 index 2ee46559e63..7a566907312 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 @@ -1,4 +1,7 @@ /*= require filtered_search/filtered_search_dropdown */ + +/* global droplabAjaxFilter */ + (() => { class DropdownUser extends gl.FilteredSearchDropdown { constructor(droplab, dropdown, input) { diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 index 7ddfdca10fa..6c66a3b0613 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -28,7 +28,7 @@ itemClicked(e, getValueFunction) { const { selected } = e.detail; - const dataValueSet = this.setDataValueIfSelected(selected); + const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(selected); if (!dataValueSet) { const value = getValueFunction(selected); @@ -46,17 +46,6 @@ this.dropdown.style.left = `${offset}px`; } - setDataValueIfSelected(selected) { - const dataValue = selected.getAttribute('data-value'); - - if (dataValue) { - gl.FilteredSearchDropdownManager.addWordToInput(dataValue); - } - - // Return boolean based on whether it was set - return dataValue !== null; - } - renderContent(forceShowList = false) { if (forceShowList && this.getCurrentHook().list.hidden) { this.getCurrentHook().list.show(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 index 7864ebf7aa1..ac71b5e4434 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 @@ -1,3 +1,5 @@ +/* global DropLab */ + (() => { class FilteredSearchDropdownManager { constructor() { diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 96131a673ef..e5b37f1e691 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -1,3 +1,5 @@ +/* global Turbolinks */ + (() => { class FilteredSearchManager { constructor() { diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 index 0507f7bbc48..57c0e8fc359 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 @@ -16,7 +16,7 @@ tokenKey, tokenValue, tokenSymbol, - } + }; } static getLastTokenObject(input) { @@ -29,7 +29,7 @@ return { key, value, - } + }; } static getLastToken(input) { @@ -40,19 +40,19 @@ const doubleQuote = '"'; const singleQuote = '\''; - while(!completeToken && i >= 0) { + while (!completeToken && i >= 0) { const isDoubleQuote = input[i] === doubleQuote; const isSingleQuote = input[i] === singleQuote; // If the second quotation is found - if ((lastQuotation === doubleQuote && input[i] === doubleQuote) || - (lastQuotation === singleQuote && input[i] === singleQuote)) { + if ((lastQuotation === doubleQuote && isDoubleQuote) || + (lastQuotation === singleQuote && isSingleQuote)) { completeQuotation = true; } // Save the first quotation - if ((input[i] === doubleQuote && lastQuotation === '') || - (input[i] === singleQuote && lastQuotation === '')) { + if ((isDoubleQuote && lastQuotation === '') || + (isSingleQuote && lastQuotation === '')) { lastQuotation = input[i]; completeQuotation = false; } @@ -60,7 +60,7 @@ if (completeQuotation && input[i] === ' ') { completeToken = true; } else { - i--; + i -= 1; } } @@ -69,7 +69,7 @@ } static processTokens(input) { - let tokens = []; + const tokens = []; let searchToken = ''; let lastToken = ''; @@ -118,16 +118,20 @@ const singleQuoteExist = singleQuoteIndex !== -1; const doubleQuoteExistOnly = doubleQuoteExist && !singleQuoteExist; - const doubleQuoteIsBeforeSingleQuote = doubleQuoteExist && singleQuoteExist && doubleQuoteIndex < singleQuoteIndex; + const doubleQuoteIsBeforeSingleQuote = + doubleQuoteExist && singleQuoteExist && doubleQuoteIndex < singleQuoteIndex; const singleQuoteExistOnly = singleQuoteExist && !doubleQuoteExist; - const singleQuoteIsBeforeDoubleQuote = doubleQuoteExist && singleQuoteExist && singleQuoteIndex < doubleQuoteIndex; + const singleQuoteIsBeforeDoubleQuote = + doubleQuoteExist && singleQuoteExist && singleQuoteIndex < doubleQuoteIndex; - if ((doubleQuoteExistOnly || doubleQuoteIsBeforeSingleQuote) && doubleQuoteOccurrences % 2 !== 0) { + if ((doubleQuoteExistOnly || doubleQuoteIsBeforeSingleQuote) + && doubleQuoteOccurrences % 2 !== 0) { // " is found and is in front of ' (if any) lastQuotation = '"'; incompleteToken = true; - } else if ((singleQuoteExistOnly || singleQuoteIsBeforeDoubleQuote) && singleQuoteOccurrences % 2 !== 0) { + } else if ((singleQuoteExistOnly || singleQuoteIsBeforeDoubleQuote) + && singleQuoteOccurrences % 2 !== 0) { // ' is found and is in front of " (if any) lastQuotation = '\''; incompleteToken = true; @@ -137,7 +141,7 @@ tokens.push({ key: keyMatch.key, value: tokenValue, - wildcard: symbolMatch ? false : true, + wildcard: !symbolMatch, }); lastToken = tokens.last(); -- cgit v1.2.1 From 2461b9b635501fc9ae98d246e0dd6d23af555351 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 13 Dec 2016 21:59:28 -0600 Subject: Fix scss lint --- app/assets/stylesheets/framework/filters.scss | 8 ++++---- app/assets/stylesheets/framework/variables.scss | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index b6c137d647a..dbe94813a93 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -39,7 +39,7 @@ padding-right: 25px; &:focus ~ .fa-filter { - color: #444; + color: $common-gray-dark; } } @@ -65,7 +65,7 @@ outline: none; &:hover .fa-times { - color: #444; + color: $common-gray-dark; } } } @@ -92,11 +92,11 @@ &:hover, &:focus { background-color: $dropdown-hover-color; - color: white; + color: $white-light; text-decoration: none; .dropdown-label-box { - border-color: white; + border-color: $white-light; border-style: solid; border-width: 2px; } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index f3cb3d33d99..cf9424ea5dd 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -266,7 +266,7 @@ $dropdown-toggle-active-border-color: darken($border-color, 14%); /* * Filtered Search */ -$dropdown-hover-color: #3B86FF; +$dropdown-hover-color: #3b86ff; /* * Buttons -- cgit v1.2.1 From 1f7659912ca73f6774c4f1b66ad4e5e48cc51068 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 13 Dec 2016 23:30:28 -0600 Subject: Add jasmine tests to dropdown utils --- .../filtered_search/dropdown_utils.js.es6 | 68 ++++++++++++ .../filtered_search/dropdown_utils_spec.js.es6 | 121 +++++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 app/assets/javascripts/filtered_search/dropdown_utils.js.es6 create mode 100644 spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 new file mode 100644 index 00000000000..3837b020fd3 --- /dev/null +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 @@ -0,0 +1,68 @@ +(() => { + class DropdownUtils { + static getEscapedText(text) { + let escapedText = text; + const hasSpace = text.indexOf(' ') !== -1; + const hasDoubleQuote = text.indexOf('"') !== -1; + + // Encapsulate value with quotes if it has spaces + // Known side effect: values's with both single and double quotes + // won't escape properly + if (hasSpace) { + if (hasDoubleQuote) { + escapedText = `'${text}'`; + } else { + // Encapsulate singleQuotes or if it hasSpace + escapedText = `"${text}"`; + } + } + + return escapedText; + } + + static filterWithSymbol(filterSymbol, item, query) { + const updatedItem = item; + const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); + const valueWithoutColon = value.slice(1).toLowerCase(); + const prefix = valueWithoutColon[0]; + const valueWithoutPrefix = valueWithoutColon.slice(1); + + const title = updatedItem.title.toLowerCase(); + + // Eg. filterSymbol = ~ for labels + const matchWithoutPrefix = + prefix === filterSymbol && title.indexOf(valueWithoutPrefix) !== -1; + const match = title.indexOf(valueWithoutColon) !== -1; + + updatedItem.droplab_hidden = !match && !matchWithoutPrefix; + return updatedItem; + } + + static filterMethod(item, query) { + const updatedItem = item; + const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); + + if (value === '') { + updatedItem.droplab_hidden = false; + } else { + updatedItem.droplab_hidden = updatedItem.hint.indexOf(value) === -1; + } + + return updatedItem; + } + + static setDataValueIfSelected(selected) { + const dataValue = selected.getAttribute('data-value'); + + if (dataValue) { + gl.FilteredSearchDropdownManager.addWordToInput(dataValue); + } + + // Return boolean based on whether it was set + return dataValue !== null; + } + } + + window.gl = window.gl || {}; + gl.DropdownUtils = DropdownUtils; +})(); diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 b/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 new file mode 100644 index 00000000000..07293b9f877 --- /dev/null +++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 @@ -0,0 +1,121 @@ +//= require filtered_search/dropdown_utils +//= require filtered_search/filtered_search_tokenizer +//= require filtered_search/filtered_search_dropdown_manager + +(() => { + describe('Dropdown Utils', () => { + describe('getEscapedText', () => { + it('should return same word when it has no space', () => { + const escaped = gl.DropdownUtils.getEscapedText('textWithoutSpace'); + expect(escaped).toBe('textWithoutSpace'); + }); + + it('should escape with double quotes', () => { + let escaped = gl.DropdownUtils.getEscapedText('text with space'); + expect(escaped).toBe('"text with space"'); + + escaped = gl.DropdownUtils.getEscapedText('won\'t fix'); + expect(escaped).toBe('"won\'t fix"'); + }); + + it('should escape with single quotes', () => { + const escaped = gl.DropdownUtils.getEscapedText('won"t fix'); + expect(escaped).toBe('\'won"t fix\''); + }); + + it('should escape with single quotes by default', () => { + const escaped = gl.DropdownUtils.getEscapedText('won"t\' fix'); + expect(escaped).toBe('\'won"t\' fix\''); + }); + }); + + describe('filterWithSymbol', () => { + const item = { + title: '@root', + }; + + beforeEach(() => { + spyOn(gl.FilteredSearchTokenizer, 'getLastTokenObject') + .and.callFake(query => ({ value: query })); + }); + + it('should filter without symbol', () => { + const updatedItem = gl.DropdownUtils.filterWithSymbol('@', item, ':roo'); + expect(updatedItem.droplab_hidden).toBe(false); + }); + + it('should filter with symbol', () => { + const updatedItem = gl.DropdownUtils.filterWithSymbol('@', item, ':@roo'); + expect(updatedItem.droplab_hidden).toBe(false); + }); + + it('should filter with invalid symbol', () => { + const updatedItem = gl.DropdownUtils.filterWithSymbol('@', item, ':#'); + expect(updatedItem.droplab_hidden).toBe(true); + }); + + it('should filter with colon', () => { + const updatedItem = gl.DropdownUtils.filterWithSymbol('@', item, ':'); + expect(updatedItem.droplab_hidden).toBe(false); + }); + }); + + describe('filterMethod', () => { + beforeEach(() => { + spyOn(gl.FilteredSearchTokenizer, 'getLastTokenObject') + .and.callFake(query => ({ value: query })); + }); + + it('should filter by hint', () => { + let updatedItem = gl.DropdownUtils.filterMethod({ + hint: 'label', + }, 'l'); + expect(updatedItem.droplab_hidden).toBe(false); + + updatedItem = gl.DropdownUtils.filterMethod({ + hint: 'label', + }, 'o'); + expect(updatedItem.droplab_hidden).toBe(true); + }); + + it('should return droplab_hidden false when item has no hint', () => { + const updatedItem = gl.DropdownUtils.filterMethod({}, ''); + expect(updatedItem.droplab_hidden).toBe(false); + }); + }); + + describe('setDataValueIfSelected', () => { + beforeEach(() => { + spyOn(gl.FilteredSearchDropdownManager, 'addWordToInput') + .and.callFake(() => {}); + }); + + it('calls addWordToInput when dataValue exists', () => { + const selected = { + getAttribute: () => 'value', + }; + + gl.DropdownUtils.setDataValueIfSelected(selected); + expect(gl.FilteredSearchDropdownManager.addWordToInput.calls.count()).toEqual(1); + }); + + it('returns true when dataValue exists', () => { + const selected = { + getAttribute: () => 'value', + }; + + const result = gl.DropdownUtils.setDataValueIfSelected(selected); + expect(result).toBe(true); + }); + + it('returns false when dataValue does not exist', () => { + const selected = { + getAttribute: () => null, + }; + + const result = gl.DropdownUtils.setDataValueIfSelected(selected); + expect(result).toBe(false); + }); + }); + }); +})(); -- cgit v1.2.1 From 9408693d30cc2af7059cff2c9ddc503a92db86a6 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 14 Dec 2016 13:39:03 -0600 Subject: Add webkit to flex --- app/assets/stylesheets/framework/filters.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index dbe94813a93..e47511940a7 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -109,11 +109,14 @@ } .dropdown-user { + display: -webkit-flex; display: flex; } .dropdown-user-details { + display: -webkit-flex; display: flex; + -webkit-flex-direction: column; flex-direction: column; } } -- cgit v1.2.1 From cf391760f19943af59ac43495a91db4126dbeb8d Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 14 Dec 2016 13:52:11 -0600 Subject: Fix HAML attributes --- app/views/shared/issuable/_search_bar.html.haml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 335552c0a26..dbef87e67cf 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -18,7 +18,7 @@ = icon('times') #js-dropdown-hint.dropdown-menu.hint-dropdown %ul{ 'data-dropdown' => true } - %li.filter-dropdown-item{ 'data-value': '' } + %li.filter-dropdown-item{ 'data-value' => '' } %button.btn.btn-link = icon('search') %span @@ -26,7 +26,7 @@ %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item %button.btn.btn-link - %i.fa{ 'class': '{{icon}}'} + %i.fa{ class: '{{icon}}'} %span.js-filter-hint {{hint}} %span.js-filter-tag.dropdown-light-content @@ -35,7 +35,7 @@ %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item %button.btn.btn-link.dropdown-user - %img.avatar.avatar-inline{ 'data-src': '{{avatar_url}}', width: '30' } + %img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', width: '30' } .dropdown-user-details %span {{name}} @@ -43,14 +43,14 @@ @{{username}} #js-dropdown-assignee.dropdown-menu %ul{ 'data-dropdown' => true } - %li.filter-dropdown-item{ 'data-value': 'none' } + %li.filter-dropdown-item{ 'data-value' => 'none' } %button.btn.btn-link No Assignee %li.divider %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item %button.btn.btn-link.dropdown-user - %img.avatar.avatar-inline{ 'data-src': '{{avatar_url}}', width: '30' } + %img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', width: '30' } .dropdown-user-details %span {{name}} @@ -58,10 +58,10 @@ @{{username}} #js-dropdown-milestone.dropdown-menu{ 'data-dropdown' => true } %ul{ 'data-dropdown' => true } - %li.filter-dropdown-item{ 'data-value': 'none' } + %li.filter-dropdown-item{ 'data-value' => 'none' } %button.btn.btn-link No Milestone - %li.filter-dropdown-item{ 'data-value': 'upcoming' } + %li.filter-dropdown-item{ 'data-value' => 'upcoming' } %button.btn.btn-link Upcoming %li.divider @@ -71,14 +71,14 @@ {{title}} #js-dropdown-label.dropdown-menu{ 'data-dropdown' => true } %ul{ 'data-dropdown' => true } - %li.filter-dropdown-item{ 'data-value': 'none' } + %li.filter-dropdown-item{ 'data-value' => 'none' } %button.btn.btn-link No Label %li.divider %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item %button.btn.btn-link - %span.dropdown-label-box{ 'style': 'background: {{color}}'} + %span.dropdown-label-box{ style => 'background: {{color}}'} %span.label-title.js-data-value {{title}} .pull-right -- cgit v1.2.1 From 3cb156dd45e7a83d83c59094894e015386d4caea Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 14 Dec 2016 14:17:28 -0600 Subject: Add tests for new common_utils functions --- .../javascripts/lib/utils/common_utils_spec.js.es6 | 25 ++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/spec/javascripts/lib/utils/common_utils_spec.js.es6 b/spec/javascripts/lib/utils/common_utils_spec.js.es6 index ef75f600898..4ba83d235c4 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js.es6 +++ b/spec/javascripts/lib/utils/common_utils_spec.js.es6 @@ -15,6 +15,7 @@ expect(gl.utils.parseUrl('" test="asf"').pathname).toEqual('/teaspoon/%22%20test=%22asf%22'); }); }); + describe('gl.utils.parseUrlPathname', () => { beforeEach(() => { spyOn(gl.utils, 'parseUrl').and.callFake(url => ({ @@ -28,5 +29,29 @@ expect(gl.utils.parseUrlPathname('some/relative/url')).toEqual('/some/relative/url'); }); }); + + describe('gl.utils.getUrlParamsArray', () => { + it('should return params array', () => { + expect(gl.utils.getUrlParamsArray() instanceof Array).toBe(true); + }); + + it('should remove the question mark from the search params', () => { + const paramsArray = gl.utils.getUrlParamsArray(); + expect(paramsArray[0][0] !== '?').toBe(true); + }); + }); + + describe('gl.utils.getParameterByName', () => { + it('should return valid parameter', () => { + const value = gl.utils.getParameterByName('reporter'); + expect(value).toBe('Console'); + }); + + it('should return invalid parameter', () => { + const value = gl.utils.getParameterByName('fakeParameter'); + expect(value).toBe(null); + }); + }); + }); })(); -- cgit v1.2.1 From 4577f1f1749976412ce03941ef712e763c9d618a Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 14 Dec 2016 14:37:17 -0600 Subject: Add text utility spec --- .../javascripts/lib/utils/common_utils_spec.js.es6 | 1 - .../javascripts/lib/utils/text_utility_spec.js.es6 | 25 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 spec/javascripts/lib/utils/text_utility_spec.js.es6 diff --git a/spec/javascripts/lib/utils/common_utils_spec.js.es6 b/spec/javascripts/lib/utils/common_utils_spec.js.es6 index 4ba83d235c4..031f9ca03c9 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js.es6 +++ b/spec/javascripts/lib/utils/common_utils_spec.js.es6 @@ -52,6 +52,5 @@ expect(value).toBe(null); }); }); - }); })(); diff --git a/spec/javascripts/lib/utils/text_utility_spec.js.es6 b/spec/javascripts/lib/utils/text_utility_spec.js.es6 new file mode 100644 index 00000000000..e97356b65d5 --- /dev/null +++ b/spec/javascripts/lib/utils/text_utility_spec.js.es6 @@ -0,0 +1,25 @@ +//= require lib/utils/text_utility + +(() => { + describe('text_utility', () => { + describe('gl.text.getTextWidth', () => { + it('returns zero width when no text is passed', () => { + expect(gl.text.getTextWidth('')).toBe(0); + }); + + it('returns zero width when no text is passed and font is passed', () => { + expect(gl.text.getTextWidth('', '100px sans-serif')).toBe(0); + }); + + it('returns width when text is passed', () => { + expect(gl.text.getTextWidth('foo') > 0).toBe(true); + }); + + it('returns bigger width when font is larger', () => { + const largeFont = gl.text.getTextWidth('foo', '100px sans-serif'); + const regular = gl.text.getTextWidth('foo', '10px sans-serif'); + expect(largeFont > regular).toBe(true); + }); + }); + }); +})(); -- cgit v1.2.1 From d93ccb8e0949e345efa9a1dcf874c73f8d1975bc Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 14 Dec 2016 15:34:04 -0600 Subject: Fix invalid style attribute operator --- app/views/shared/issuable/_search_bar.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index dbef87e67cf..aca39941381 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -78,7 +78,7 @@ %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item %button.btn.btn-link - %span.dropdown-label-box{ style => 'background: {{color}}'} + %span.dropdown-label-box{ style: 'background: {{color}}'} %span.label-title.js-data-value {{title}} .pull-right -- cgit v1.2.1 From f7b6361124c3abb4380035eaf3b7625433686543 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 14 Dec 2016 21:51:33 -0600 Subject: Add jasmine tests for filtered search dropdown manager --- .../filtered_search_dropdown_manager_spec.js.es6 | 57 ++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 diff --git a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 new file mode 100644 index 00000000000..11765d7d7ea --- /dev/null +++ b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 @@ -0,0 +1,57 @@ +//= require filtered_search/filtered_search_tokenizer +//= require filtered_search/filtered_search_dropdown_manager + +(() => { + describe('Filtered Search Dropdown Manager', () => { + describe('addWordToInput', () => { + describe('add word and when lastToken is an empty object', () => { + function getInput() { + return document.querySelector('.filtered-search'); + } + + beforeEach(() => { + spyOn(gl.FilteredSearchTokenizer, 'processTokens') + .and.callFake(query => ({ + lastToken: {} + }) + ); + + const input = document.createElement('input'); + input.classList.add('filtered-search'); + document.body.appendChild(input); + + expect(input.value).toBe(''); + }); + + afterEach(() => { + document.querySelector('.filtered-search').outerHTML = ''; + }); + + it('should add word', () => { + gl.FilteredSearchDropdownManager.addWordToInput('firstWord'); + expect(getInput().value).toBe('firstWord'); + }); + + it('should not add space before first word', () => { + gl.FilteredSearchDropdownManager.addWordToInput('firstWord', true); + expect(getInput().value).toBe('firstWord'); + }); + + it('should not add space before second word by default', () => { + gl.FilteredSearchDropdownManager.addWordToInput('firstWord'); + expect(getInput().value).toBe('firstWord'); + gl.FilteredSearchDropdownManager.addWordToInput('secondWord'); + expect(getInput().value).toBe('firstWordsecondWord'); + }); + + it('should add space before new word when addSpace is passed', () => { + expect(getInput().value).toBe(''); + gl.FilteredSearchDropdownManager.addWordToInput('firstWord'); + expect(getInput().value).toBe('firstWord'); + gl.FilteredSearchDropdownManager.addWordToInput('secondWord', true); + expect(getInput().value).toBe('firstWord secondWord'); + }); + }); + }); + }); +})(); -- cgit v1.2.1 From 410e223597c78df30ddcf71a859b65ea05729794 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 08:34:34 -0600 Subject: Fix search autocomplete jasmine test --- spec/javascripts/search_autocomplete_spec.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index e13c4ad772c..2d3f44e7980 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -11,6 +11,7 @@ (function() { var addBodyAttributes, assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget; + var userName = 'root'; widget = null; @@ -19,6 +20,7 @@ window.gon || (window.gon = {}); window.gon.current_user_id = userId; + window.gon.current_username = userName; dashboardIssuesPath = '/dashboard/issues'; @@ -93,8 +95,8 @@ assertLinks = function(list, issuesPath, mrsPath) { var a1, a2, a3, a4, issuesAssignedToMeLink, issuesIHaveCreatedLink, mrsAssignedToMeLink, mrsIHaveCreatedLink; - issuesAssignedToMeLink = issuesPath + "/?assignee_id=" + userId; - issuesIHaveCreatedLink = issuesPath + "/?author_id=" + userId; + issuesAssignedToMeLink = issuesPath + "/?assignee_username=" + userName; + issuesIHaveCreatedLink = issuesPath + "/?author_username=" + userName; mrsAssignedToMeLink = mrsPath + "/?assignee_id=" + userId; mrsIHaveCreatedLink = mrsPath + "/?author_id=" + userId; a1 = "a[href='" + issuesAssignedToMeLink + "']"; -- cgit v1.2.1 From 6662f034be3d576fba32d0a0acdc2cb95145a761 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 08:40:25 -0600 Subject: Fix eslint --- .../filtered_search/filtered_search_dropdown_manager_spec.js.es6 | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 index 11765d7d7ea..4a358bd43e3 100644 --- a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 +++ b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 @@ -11,10 +11,9 @@ beforeEach(() => { spyOn(gl.FilteredSearchTokenizer, 'processTokens') - .and.callFake(query => ({ - lastToken: {} - }) - ); + .and.callFake(() => ({ + lastToken: {}, + })); const input = document.createElement('input'); input.classList.add('filtered-search'); -- cgit v1.2.1 From 657ac981acdcd2b070d838e530a2620c4db8bf04 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 09:07:34 -0600 Subject: Fix spinach tests --- app/assets/javascripts/dispatcher.js.es6 | 2 +- features/project/issues/filter_labels.feature | 28 -------------- features/project/issues/issues.feature | 56 --------------------------- 3 files changed, 1 insertion(+), 85 deletions(-) delete mode 100644 features/project/issues/filter_labels.feature diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 9a76131b87f..1e9111f4718 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -84,7 +84,7 @@ break; case 'projects:merge_requests:index': case 'projects:issues:index': - if(gl.FilteredSearchManager) { + if(document.querySelector('.filtered-search') && gl.FilteredSearchManager) { new gl.FilteredSearchManager(); } Issuable.init(); diff --git a/features/project/issues/filter_labels.feature b/features/project/issues/filter_labels.feature deleted file mode 100644 index 49d7a3b9af2..00000000000 --- a/features/project/issues/filter_labels.feature +++ /dev/null @@ -1,28 +0,0 @@ -@project_issues -Feature: Project Issues Filter Labels - Background: - Given I sign in as a user - And I own project "Shop" - And project "Shop" has labels: "bug", "feature", "enhancement" - And project "Shop" has issue "Bugfix1" with labels: "bug", "feature" - And project "Shop" has issue "Bugfix2" with labels: "bug", "enhancement" - And project "Shop" has issue "Feature1" with labels: "feature" - Given I visit project "Shop" issues page - - @javascript - Scenario: I filter by one label - Given I click link "bug" - And I click "dropdown close button" - Then I should see "Bugfix1" in issues list - And I should see "Bugfix2" in issues list - And I should not see "Feature1" in issues list - - # TODO: make labels filter works according to this scanario - # right now it looks for label 1 OR label 2. Old behaviour (this test) was - # all issues that have both label 1 AND label 2 - #Scenario: I filter by two labels - #Given I click link "bug" - #And I click link "feature" - #Then I should see "Bugfix1" in issues list - #And I should not see "Bugfix2" in issues list - #And I should not see "Feature1" in issues list diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature index 80670063ea0..b2b4fe72220 100644 --- a/features/project/issues/issues.feature +++ b/features/project/issues/issues.feature @@ -26,12 +26,6 @@ Feature: Project Issues Given I click link "Release 0.4" Then I should see issue "Release 0.4" - @javascript - Scenario: I filter by author - Given I add a user to project "Shop" - And I click "author" dropdown - Then I see current user as the first user - Scenario: I submit new unassigned issue Given I click link "New Issue" And I submit new issue "500 error on profile" @@ -84,56 +78,6 @@ Feature: Project Issues And I sort the list by "Least popular" Then The list should be sorted by "Least popular" - @javascript - Scenario: I search issue - Given I fill in issue search with "Re" - Then I should see "Release 0.4" in issues - And I should not see "Release 0.3" in issues - And I should not see "Tweet control" in issues - - @javascript - Scenario: I search issue that not exist - Given I fill in issue search with "Bu" - Then I should not see "Release 0.4" in issues - And I should not see "Release 0.3" in issues - - @javascript - Scenario: I search all issues - Given I click link "All" - And I fill in issue search with ".3" - Then I should see "Release 0.3" in issues - And I should not see "Release 0.4" in issues - - @javascript - Scenario: Search issues when search string exactly matches issue description - Given project 'Shop' has issue 'Bugfix1' with description: 'Description for issue1' - And I fill in issue search with 'Description for issue1' - Then I should see 'Bugfix1' in issues - And I should not see "Release 0.4" in issues - And I should not see "Release 0.3" in issues - And I should not see "Tweet control" in issues - - @javascript - Scenario: Search issues when search string partially matches issue description - Given project 'Shop' has issue 'Bugfix1' with description: 'Description for issue1' - And project 'Shop' has issue 'Feature1' with description: 'Feature submitted for issue1' - And I fill in issue search with 'issue1' - Then I should see 'Feature1' in issues - Then I should see 'Bugfix1' in issues - And I should not see "Release 0.4" in issues - And I should not see "Release 0.3" in issues - And I should not see "Tweet control" in issues - - @javascript - Scenario: Search issues when search string matches no issue description - Given project 'Shop' has issue 'Bugfix1' with description: 'Description for issue1' - And I fill in issue search with 'Rock and roll' - Then I should not see 'Bugfix1' in issues - And I should not see "Release 0.4" in issues - And I should not see "Release 0.3" in issues - And I should not see "Tweet control" in issues - - # Markdown Scenario: Headers inside the description should have ids generated for them. -- cgit v1.2.1 From 78dd92b730063371742b6487ae2526d6cc0943b1 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 11:20:05 -0600 Subject: Improve styling of hover states --- app/assets/stylesheets/framework/filters.scss | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index e47511940a7..8b7cb245420 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -89,16 +89,22 @@ overflow-y: hidden; border-radius: 0; + .dropdown-label-box { + border-color: $white-light; + border-style: solid; + border-width: 1px; + width: 17px; + height: 17px; + } + &:hover, &:focus { background-color: $dropdown-hover-color; color: $white-light; text-decoration: none; - .dropdown-label-box { + .avatar { border-color: $white-light; - border-style: solid; - border-width: 2px; } } } -- cgit v1.2.1 From 89204c22f15720080f71e424e50bad9485a49172 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 12:19:03 -0600 Subject: Add specs for addWordToInput --- .../filtered_search_dropdown_manager_spec.js.es6 | 76 ++++++++++++++++------ 1 file changed, 55 insertions(+), 21 deletions(-) diff --git a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 index 4a358bd43e3..17d414aaad1 100644 --- a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 +++ b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 @@ -4,51 +4,85 @@ (() => { describe('Filtered Search Dropdown Manager', () => { describe('addWordToInput', () => { - describe('add word and when lastToken is an empty object', () => { - function getInput() { - return document.querySelector('.filtered-search'); - } + function getInputValue() { + return document.querySelector('.filtered-search').value; + } + beforeEach(() => { + const input = document.createElement('input'); + input.classList.add('filtered-search'); + document.body.appendChild(input); + + expect(input.value).toBe(''); + }); + + afterEach(() => { + document.querySelector('.filtered-search').outerHTML = ''; + }); + + describe('input has no existing value', () => { beforeEach(() => { spyOn(gl.FilteredSearchTokenizer, 'processTokens') .and.callFake(() => ({ lastToken: {}, })); - - const input = document.createElement('input'); - input.classList.add('filtered-search'); - document.body.appendChild(input); - - expect(input.value).toBe(''); - }); - - afterEach(() => { - document.querySelector('.filtered-search').outerHTML = ''; }); it('should add word', () => { gl.FilteredSearchDropdownManager.addWordToInput('firstWord'); - expect(getInput().value).toBe('firstWord'); + expect(getInputValue()).toBe('firstWord'); }); it('should not add space before first word', () => { gl.FilteredSearchDropdownManager.addWordToInput('firstWord', true); - expect(getInput().value).toBe('firstWord'); + expect(getInputValue()).toBe('firstWord'); }); it('should not add space before second word by default', () => { gl.FilteredSearchDropdownManager.addWordToInput('firstWord'); - expect(getInput().value).toBe('firstWord'); + expect(getInputValue()).toBe('firstWord'); gl.FilteredSearchDropdownManager.addWordToInput('secondWord'); - expect(getInput().value).toBe('firstWordsecondWord'); + expect(getInputValue()).toBe('firstWordsecondWord'); }); it('should add space before new word when addSpace is passed', () => { - expect(getInput().value).toBe(''); + expect(getInputValue()).toBe(''); gl.FilteredSearchDropdownManager.addWordToInput('firstWord'); - expect(getInput().value).toBe('firstWord'); + expect(getInputValue()).toBe('firstWord'); gl.FilteredSearchDropdownManager.addWordToInput('secondWord', true); - expect(getInput().value).toBe('firstWord secondWord'); + expect(getInputValue()).toBe('firstWord secondWord'); + }); + }); + + describe('input has exsting value', () => { + it('should only add the remaining characters of the word', () => { + const lastToken = { + key: 'author', + value: 'roo', + }; + + spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.callFake(() => ({ + lastToken, + })); + + document.querySelector('.filtered-search').value = `${lastToken.key}:${lastToken.value}`; + gl.FilteredSearchDropdownManager.addWordToInput('root'); + expect(getInputValue()).toBe('author:root'); + }); + + it('should only add the remaining characters of the word (contains space)', () => { + const lastToken = { + key: 'label', + value: 'test me', + }; + + spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.callFake(() => ({ + lastToken, + })); + + document.querySelector('.filtered-search').value = `${lastToken.key}:"${lastToken.value}"`; + gl.FilteredSearchDropdownManager.addWordToInput('~\'"test me"\''); + expect(getInputValue()).toBe('label:~\'"test me"\''); }); }); }); -- cgit v1.2.1 From 776f1aaae4a8125f6f46ed4a095566a00ea2aa45 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 12:54:24 -0600 Subject: Add specs for filtered search token keys --- .../filtered_search_token_keys.js.es6 | 4 + .../filtered_search_token_keys_spec.js.es6 | 104 +++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6 diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 index a1830d13e5f..6bd9cb06362 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 @@ -44,6 +44,10 @@ return tokenKeys; } + static getConditions() { + return conditions; + } + static searchByKey(key) { return tokenKeys.find(tokenKey => tokenKey.key === key) || null; } diff --git a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6 new file mode 100644 index 00000000000..6df7c0e44ef --- /dev/null +++ b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6 @@ -0,0 +1,104 @@ +//= require extensions/array +//= require filtered_search/filtered_search_token_keys + +(() => { + describe('Filtered Search Token Keys', () => { + describe('get', () => { + let tokenKeys; + + beforeEach(() => { + tokenKeys = gl.FilteredSearchTokenKeys.get(); + }); + + it('should return tokenKeys', () => { + expect(tokenKeys !== null).toBe(true); + }); + + it('should return tokenKeys as an array', () => { + expect(tokenKeys instanceof Array).toBe(true); + }); + }); + + describe('getConditions', () => { + let conditions; + + beforeEach(() => { + conditions = gl.FilteredSearchTokenKeys.getConditions(); + }); + + it('should return conditions', () => { + expect(conditions !== null).toBe(true); + }); + + it('should return conditions as an array', () => { + expect(conditions instanceof Array).toBe(true); + }); + }); + + describe('searchByKey', () => { + it('should return null when key not found', () => { + const tokenKey = gl.FilteredSearchTokenKeys.searchByKey('notakey'); + expect(tokenKey === null).toBe(true); + }); + + it('should return tokenKey when found by key', () => { + const tokenKeys = gl.FilteredSearchTokenKeys.get(); + const result = gl.FilteredSearchTokenKeys.searchByKey(tokenKeys[0].key); + expect(result).toEqual(tokenKeys[0]); + }); + }); + + describe('searchBySymbol', () => { + it('should return null when symbol not found', () => { + const tokenKey = gl.FilteredSearchTokenKeys.searchBySymbol('notasymbol'); + expect(tokenKey === null).toBe(true); + }); + + it('should return tokenKey when found by symbol', () => { + const tokenKeys = gl.FilteredSearchTokenKeys.get(); + const result = gl.FilteredSearchTokenKeys.searchBySymbol(tokenKeys[0].symbol); + expect(result).toEqual(tokenKeys[0]); + }); + }); + + describe('searchByKeyParam', () => { + it('should return null when key param not found', () => { + const tokenKey = gl.FilteredSearchTokenKeys.searchByKeyParam('notakeyparam'); + expect(tokenKey === null).toBe(true); + }); + + it('should return tokenKey when found by key param', () => { + const tokenKeys = gl.FilteredSearchTokenKeys.get(); + const result = gl.FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`); + expect(result).toEqual(tokenKeys[0]); + }); + }); + + describe('searchByConditionUrl', () => { + it('should return null when condition url not found', () => { + const condition = gl.FilteredSearchTokenKeys.searchByConditionUrl(null); + expect(condition === null).toBe(true); + }); + + it('should return condition when found by url', () => { + const conditions = gl.FilteredSearchTokenKeys.getConditions(); + const result = gl.FilteredSearchTokenKeys.searchByConditionUrl(conditions[0].url); + expect(result).toBe(conditions[0]); + }); + }); + + describe('searchByConditionKeyValue', () => { + it('should return null when condition tokenKey and value not found', () => { + const condition = gl.FilteredSearchTokenKeys.searchByConditionKeyValue(null, null); + expect(condition === null).toBe(true); + }); + + it('should return condition when found by tokenKey and value', () => { + const conditions = gl.FilteredSearchTokenKeys.getConditions(); + const result = gl.FilteredSearchTokenKeys + .searchByConditionKeyValue(conditions[0].tokenKey, conditions[0].value); + expect(result).toEqual(conditions[0]); + }); + }); + }); +})(); -- cgit v1.2.1 From c94b64813399633ccb3a20c0f09c2e70aa7bbc0c Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 14:16:29 -0600 Subject: Add specs to filtered search tokenizer --- .../filtered_search_tokenizer_spec.js.es6 | 271 +++++++++++++++++++++ 1 file changed, 271 insertions(+) create mode 100644 spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 diff --git a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 new file mode 100644 index 00000000000..c93f163e763 --- /dev/null +++ b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 @@ -0,0 +1,271 @@ +//= require extensions/array +//= require filtered_search/filtered_search_token_keys +//= require filtered_search/filtered_search_tokenizer + +(() => { + describe('Filtered Search Tokenizer', () => { + describe('parseToken', () => { + it('should return key, value and symbol', () => { + const { tokenKey, tokenValue, tokenSymbol } = gl.FilteredSearchTokenizer + .parseToken('author:@user'); + + expect(tokenKey).toBe('author'); + expect(tokenValue).toBe('@user'); + expect(tokenSymbol).toBe('@'); + }); + + it('should return value with spaces', () => { + const { tokenKey, tokenValue, tokenSymbol } = gl.FilteredSearchTokenizer + .parseToken('label:~"test me"'); + + expect(tokenKey).toBe('label'); + expect(tokenValue).toBe('~"test me"'); + expect(tokenSymbol).toBe('~'); + }); + }); + + describe('getLastTokenObject', () => { + beforeEach(() => { + spyOn(gl.FilteredSearchTokenizer, 'getLastToken').and.callFake(input => input); + }); + + it('should return key and value', () => { + const { key, value } = gl.FilteredSearchTokenizer.getLastTokenObject('author:@root'); + expect(key).toBe('author'); + expect(value).toBe(':@root'); + }); + + describe('string without colon', () => { + let lastTokenObject; + + beforeEach(() => { + lastTokenObject = gl.FilteredSearchTokenizer.getLastTokenObject('author'); + }); + + it('should return key as an empty string', () => { + expect(lastTokenObject.key).toBe(''); + }); + + it('should return input as value', () => { + expect(lastTokenObject.value).toBe('author'); + }); + }); + }); + + describe('getLastToken', () => { + it('returns entire string when there is only one word', () => { + const lastToken = gl.FilteredSearchTokenizer.getLastToken('input'); + expect(lastToken).toBe('input'); + }); + + it('returns last word when there are multiple words', () => { + const lastToken = gl.FilteredSearchTokenizer.getLastToken('this is a few words'); + expect(lastToken).toBe('words'); + }); + + it('returns last token when there are multiple tokens', () => { + const lastToken = gl.FilteredSearchTokenizer + .getLastToken('label:fun author:root milestone:2.0'); + expect(lastToken).toBe('milestone:2.0'); + }); + + it('returns last token containing spaces escaped by double quotes', () => { + const lastToken = gl.FilteredSearchTokenizer + .getLastToken('label:fun author:root milestone:2.0 label:~"Feature Proposal"'); + expect(lastToken).toBe('label:~"Feature Proposal"'); + }); + + it('returns last token containing spaces escaped by single quotes', () => { + const lastToken = gl.FilteredSearchTokenizer + .getLastToken('label:fun author:root milestone:2.0 label:~\'Feature Proposal\''); + expect(lastToken).toBe('label:~\'Feature Proposal\''); + }); + + it('returns last token containing special characters', () => { + const lastToken = gl.FilteredSearchTokenizer + .getLastToken('label:fun author:root milestone:2.0 label:~!@#$%^&*()'); + expect(lastToken).toBe('label:~!@#$%^&*()'); + }); + }); + + describe('processTokens', () => { + describe('input does not contain any tokens', () => { + let results; + beforeEach(() => { + results = gl.FilteredSearchTokenizer.processTokens('searchTerm'); + }); + + it('returns input as searchToken', () => { + expect(results.searchToken).toBe('searchTerm'); + }); + + it('returns tokens as an empty array', () => { + expect(results.tokens.length).toBe(0); + }); + + it('returns lastToken equal to searchToken', () => { + expect(results.lastToken).toBe(results.searchToken); + }); + }); + + describe('input contains only tokens', () => { + let results; + beforeEach(() => { + results = gl.FilteredSearchTokenizer + .processTokens('author:@root label:~"Very Important" milestone:%v1.0 assignee:none'); + }); + + it('returns searchToken as an empty string', () => { + expect(results.searchToken).toBe(''); + }); + + it('returns tokens array of size equal to the number of tokens in input', () => { + expect(results.tokens.length).toBe(4); + }); + + it('returns tokens array that matches the tokens found in input', () => { + expect(results.tokens[0].key).toBe('author'); + expect(results.tokens[0].value).toBe('@root'); + expect(results.tokens[0].wildcard).toBe(false); + + expect(results.tokens[1].key).toBe('label'); + expect(results.tokens[1].value).toBe('~Very Important'); + expect(results.tokens[1].wildcard).toBe(false); + + expect(results.tokens[2].key).toBe('milestone'); + expect(results.tokens[2].value).toBe('%v1.0'); + expect(results.tokens[2].wildcard).toBe(false); + + expect(results.tokens[3].key).toBe('assignee'); + expect(results.tokens[3].value).toBe('none'); + expect(results.tokens[3].wildcard).toBe(true); + }); + + it('returns lastToken equal to the last object in the tokens array', () => { + expect(results.tokens[3]).toBe(results.lastToken); + }); + }); + + describe('input starts with search value and ends with tokens', () => { + let results; + beforeEach(() => { + results = gl.FilteredSearchTokenizer + .processTokens('searchTerm anotherSearchTerm milestone:none'); + }); + + it('returns searchToken', () => { + expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); + }); + + it('returns correct number of tokens', () => { + expect(results.tokens.length).toBe(1); + }); + + it('returns correct tokens', () => { + expect(results.tokens[0].key).toBe('milestone'); + expect(results.tokens[0].value).toBe('none'); + expect(results.tokens[0].wildcard).toBe(true); + }); + + it('returns lastToken', () => { + expect(results.tokens[0]).toBe(results.lastToken); + }); + }); + + describe('input starts with token and ends with search value', () => { + let results; + beforeEach(() => { + results = gl.FilteredSearchTokenizer + .processTokens('assignee:@user searchTerm'); + }); + + it('returns searchToken', () => { + expect(results.searchToken).toBe('searchTerm'); + }); + + it('returns correct number of tokens', () => { + expect(results.tokens.length).toBe(1); + }); + + it('returns correct tokens', () => { + expect(results.tokens[0].key).toBe('assignee'); + expect(results.tokens[0].value).toBe('@user'); + expect(results.tokens[0].wildcard).toBe(false); + }); + + it('returns lastToken as the searchTerm', () => { + expect(results.lastToken).toBe(results.searchToken); + }); + }); + + describe('input contains search value wrapped between tokens', () => { + let results; + beforeEach(() => { + results = gl.FilteredSearchTokenizer + .processTokens('author:@root label:~"Won\'t fix" searchTerm anotherSearchTerm milestone:none'); + }); + + it('returns searchToken', () => { + expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); + }); + + it('returns correct number of tokens', () => { + expect(results.tokens.length).toBe(3); + }); + + + it('returns tokens array in the order it was processed', () => { + expect(results.tokens[0].key).toBe('author'); + expect(results.tokens[0].value).toBe('@root'); + expect(results.tokens[0].wildcard).toBe(false); + + expect(results.tokens[1].key).toBe('label'); + expect(results.tokens[1].value).toBe('~Won\'t fix'); + expect(results.tokens[1].wildcard).toBe(false); + + expect(results.tokens[2].key).toBe('milestone'); + expect(results.tokens[2].value).toBe('none'); + expect(results.tokens[2].wildcard).toBe(true); + }); + + it('returns lastToken', () => { + expect(results.tokens[2]).toBe(results.lastToken); + }); + }); + + describe('input search value is spaced in between tokens', () => { + let results; + beforeEach(() => { + results = gl.FilteredSearchTokenizer + .processTokens('author:@root searchTerm assignee:none anotherSearchTerm label:~Doing'); + }); + + it('returns searchToken', () => { + expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); + }); + + it('returns correct number of tokens', () => { + expect(results.tokens.length).toBe(3); + }); + + it('returns tokens array in the order it was processed', () => { + expect(results.tokens[0].key).toBe('author'); + expect(results.tokens[0].value).toBe('@root'); + expect(results.tokens[0].wildcard).toBe(false); + + expect(results.tokens[1].key).toBe('assignee'); + expect(results.tokens[1].value).toBe('none'); + expect(results.tokens[1].wildcard).toBe(true); + + expect(results.tokens[2].key).toBe('label'); + expect(results.tokens[2].value).toBe('~Doing'); + expect(results.tokens[2].wildcard).toBe(false); + }); + + it('returns lastToken', () => { + expect(results.tokens[2]).toBe(results.lastToken); + }); + }); + }); + }); +})(); -- cgit v1.2.1 From 43850df63e342f912d8a8b0989364be6b26e655f Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 15:10:45 -0600 Subject: Add user symbol for search spec --- spec/features/search_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb index 9a7079848a5..a05b83959fb 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -170,7 +170,7 @@ describe "Search", feature: true do sleep 2 expect(page).to have_selector('.filtered-search') - expect(find('.filtered-search').value).to eq("assignee:#{user.username}") + expect(find('.filtered-search').value).to eq("assignee:@#{user.username}") end it 'takes user to her issues page when issues authored is clicked' do @@ -178,7 +178,7 @@ describe "Search", feature: true do sleep 2 expect(page).to have_selector('.filtered-search') - expect(find('.filtered-search').value).to eq("author:#{user.username}") + expect(find('.filtered-search').value).to eq("author:@#{user.username}") end it 'takes user to her MR page when MR assigned is clicked' do -- cgit v1.2.1 From cc34439d56b35f2175c979ed85b655280bbdd899 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 15:57:35 -0600 Subject: Fix RSS feed test --- spec/features/issues/filter_issues_spec.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index cbb11b790ec..391c8905630 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -3,8 +3,8 @@ require 'rails_helper' describe 'Filter issues', feature: true do include WaitForAjax - let!(:project) { create(:project) } let!(:group) { create(:group) } + let!(:project) { create(:project, group: group) } let!(:user) { create(:user) } let!(:user2) { create(:user) } let!(:milestone) { create(:milestone, project: project) } @@ -652,30 +652,30 @@ describe 'Filter issues', feature: true do describe 'RSS feeds' do it 'updates atom feed link for project issues' do - visit namespace_project_issues_path(project.namespace, project, milestone_title: '', assignee_id: user.id) + visit namespace_project_issues_path(project.namespace, project, milestone_title: milestone.title, assignee_id: user.id) link = find('.nav-controls a', text: 'Subscribe') params = CGI::parse(URI.parse(link[:href]).query) auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) auto_discovery_params = CGI::parse(URI.parse(auto_discovery_link[:href]).query) expect(params).to include('private_token' => [user.private_token]) - expect(params).to include('milestone_title' => ['']) + expect(params).to include('milestone_title' => [milestone.title]) expect(params).to include('assignee_id' => [user.id.to_s]) expect(auto_discovery_params).to include('private_token' => [user.private_token]) - expect(auto_discovery_params).to include('milestone_title' => ['']) + expect(auto_discovery_params).to include('milestone_title' => [milestone.title]) expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s]) end it 'updates atom feed link for group issues' do - visit issues_group_path(group, milestone_title: '', assignee_id: user.id) + visit issues_group_path(group, milestone_title: milestone.title, assignee_id: user.id) link = find('.nav-controls a', text: 'Subscribe') params = CGI::parse(URI.parse(link[:href]).query) auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) auto_discovery_params = CGI::parse(URI.parse(auto_discovery_link[:href]).query) expect(params).to include('private_token' => [user.private_token]) - expect(params).to include('milestone_title' => ['']) + expect(params).to include('milestone_title' => [milestone.title]) expect(params).to include('assignee_id' => [user.id.to_s]) expect(auto_discovery_params).to include('private_token' => [user.private_token]) - expect(auto_discovery_params).to include('milestone_title' => ['']) + expect(auto_discovery_params).to include('milestone_title' => [milestone.title]) expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s]) end end -- cgit v1.2.1 From 4f774c940f1bbcadaead168e6ee5dd5c54864c7f Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 16:11:37 -0600 Subject: Remove if issue.boards since search bar does not display on issue boards page --- app/views/shared/issuable/_search_bar.html.haml | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index aca39941381..896769768eb 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -82,20 +82,7 @@ %span.label-title.js-data-value {{title}} .pull-right - - if boards_page - #js-boards-seach.issue-boards-search - %input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" } - - if can?(current_user, :admin_list, @project) - .dropdown.pull-right - %button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } } - Create new list - .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable - = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Create a new list" } - - if can?(current_user, :admin_label, @project) - = render partial: "shared/issuable/label_page_create" - = dropdown_loading - - else - = render 'shared/sort_dropdown' + = render 'shared/sort_dropdown' - if @bulk_edit .issues_bulk_update.hide -- cgit v1.2.1 From 206d442b7a11312a759530b139bb481f4dc48718 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 16:11:45 -0600 Subject: Refine search bar specs --- spec/features/issues/search_bar_spec.rb | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/spec/features/issues/search_bar_spec.rb b/spec/features/issues/search_bar_spec.rb index 1d632671fe2..d0abdc284ea 100644 --- a/spec/features/issues/search_bar_spec.rb +++ b/spec/features/issues/search_bar_spec.rb @@ -1,22 +1,20 @@ require 'rails_helper' -describe 'Search bar', feature: true do +describe 'Search bar', js: true, feature: true do include WaitForAjax - let!(:project) { create(:project) } - let!(:group) { create(:group) } - let!(:user) { create(:user) } + let!(:project) { create(:empty_project) } + let!(:user) { create(:user) } before do project.team << [user, :master] - group.add_developer(user) login_as(user) create(:issue, project: project) visit namespace_project_issues_path(project.namespace, project) end - describe 'clear search button', js: true do + describe 'clear search button' do it 'clears text' do search_text = 'search_text' filtered_search = find('.filtered-search') -- cgit v1.2.1 From 63ff055eccefd97cb7014aab94a624f338057dd6 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 16:18:16 -0600 Subject: Change CGI::Parse to CGI.Parse --- spec/features/issues/filter_issues_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index 391c8905630..8911b919cf7 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -654,9 +654,9 @@ describe 'Filter issues', feature: true do it 'updates atom feed link for project issues' do visit namespace_project_issues_path(project.namespace, project, milestone_title: milestone.title, assignee_id: user.id) link = find('.nav-controls a', text: 'Subscribe') - params = CGI::parse(URI.parse(link[:href]).query) + params = CGI.parse(URI.parse(link[:href]).query) auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) - auto_discovery_params = CGI::parse(URI.parse(auto_discovery_link[:href]).query) + auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query) expect(params).to include('private_token' => [user.private_token]) expect(params).to include('milestone_title' => [milestone.title]) expect(params).to include('assignee_id' => [user.id.to_s]) @@ -668,9 +668,9 @@ describe 'Filter issues', feature: true do it 'updates atom feed link for group issues' do visit issues_group_path(group, milestone_title: milestone.title, assignee_id: user.id) link = find('.nav-controls a', text: 'Subscribe') - params = CGI::parse(URI.parse(link[:href]).query) + params = CGI.parse(URI.parse(link[:href]).query) auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) - auto_discovery_params = CGI::parse(URI.parse(auto_discovery_link[:href]).query) + auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query) expect(params).to include('private_token' => [user.private_token]) expect(params).to include('milestone_title' => [milestone.title]) expect(params).to include('assignee_id' => [user.id.to_s]) -- cgit v1.2.1 From 98189f37c94a6c97d7420e66deb25a000ee65afc Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 18:53:51 -0600 Subject: Remove trailing whitespace --- spec/features/issues/filter_issues_spec.rb | 40 +++++++++++++++--------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index 8911b919cf7..0b94bcc4e3f 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -67,19 +67,19 @@ describe 'Filter issues', feature: true do assignee: user) issue.labels << bug_label - issue_with_caps_label = create(:issue, - title: "issue by assignee with searchTerm and label", - project: project, - milestone: milestone, - author: user, + issue_with_caps_label = create(:issue, + title: "issue by assignee with searchTerm and label", + project: project, + milestone: milestone, + author: user, assignee: user) issue_with_caps_label.labels << caps_sensitive_label - issue_with_everything = create(:issue, - title: "Bug report with everything you thought was possible", - project: project, - milestone: milestone, - author: user, + issue_with_everything = create(:issue, + title: "Bug report with everything you thought was possible", + project: project, + milestone: milestone, + author: user, assignee: user) issue_with_everything.labels << bug_label issue_with_everything.labels << caps_sensitive_label @@ -590,17 +590,17 @@ describe 'Filter issues', feature: true do context 'sorting', js: true do it 'sorts by oldest updated' do - create(:issue, - title: '3 days ago', - project: project, - author: user, + create(:issue, + title: '3 days ago', + project: project, + author: user, created_at: 3.days.ago, updated_at: 3.days.ago) - old_issue = create(:issue, - title: '5 days ago', - project: project, - author: user, + old_issue = create(:issue, + title: '5 days ago', + project: project, + author: user, created_at: 5.days.ago, updated_at: 5.days.ago) @@ -609,10 +609,10 @@ describe 'Filter issues', feature: true do sort_toggle = find('.filtered-search-container .dropdown-toggle') sort_toggle.click - + find('.filtered-search-container .dropdown-menu li a', text: 'Oldest updated').click wait_for_ajax - + expect(find('.issues-list .issue:first-of-type .issue-title-text a')).to have_content(old_issue.title) end end -- cgit v1.2.1 From d19303cbe1e9813f5fe2409908c7f89616ec5eac Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 23:08:49 -0600 Subject: Fix dropdown hint reset when changing tabs --- app/assets/javascripts/filtered_search/dropdown_hint.js.es6 | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index 34079b25846..b9f552b62b9 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -59,7 +59,15 @@ renderContent() { this.droplab.changeHookList(this.hookId, this.dropdown, [droplabFilter], this.config); - this.droplab.setData(this.hookId, dropdownData); + + // Clone dropdownData to prevent it from being + // changed due to pass by reference + const data = []; + dropdownData.forEach((item) => { + data.push(Object.assign({}, item)); + }); + + this.droplab.setData(this.hookId, data); } init() { -- cgit v1.2.1 From 61680a2d9833cc3ef63b9e76930f47e44258f30d Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 23:20:31 -0600 Subject: Add selected tagName check for itemClicked --- .../javascripts/filtered_search/dropdown_hint.js.es6 | 20 +++++++++++--------- .../filtered_search/filtered_search_dropdown.js.es6 | 15 +++++++++------ 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index b9f552b62b9..bdcece61984 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -35,18 +35,20 @@ itemClicked(e) { const { selected } = e.detail; - if (selected.hasAttribute('data-value')) { + if (selected.tagName === 'LI') { + if (selected.hasAttribute('data-value')) { this.dismissDropdown(); - } else { - const token = selected.querySelector('.js-filter-hint').innerText.trim(); - const tag = selected.querySelector('.js-filter-tag').innerText.trim(); + } else { + const token = selected.querySelector('.js-filter-hint').innerText.trim(); + const tag = selected.querySelector('.js-filter-tag').innerText.trim(); - if (tag.length) { - gl.FilteredSearchDropdownManager - .addWordToInput(this.getSelectedTextWithoutEscaping(token)); + if (tag.length) { + gl.FilteredSearchDropdownManager + .addWordToInput(this.getSelectedTextWithoutEscaping(token)); + } + this.dismissDropdown(); + this.dispatchInputEvent(); } - this.dismissDropdown(); - this.dispatchInputEvent(); } } diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 index 6c66a3b0613..68014e27462 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -28,14 +28,17 @@ itemClicked(e, getValueFunction) { const { selected } = e.detail; - const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(selected); - if (!dataValueSet) { - const value = getValueFunction(selected); - gl.FilteredSearchDropdownManager.addWordToInput(value); - } + if (selected.tagName === 'LI') { + const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(selected); + + if (!dataValueSet) { + const value = getValueFunction(selected); + gl.FilteredSearchDropdownManager.addWordToInput(value); + } - this.dismissDropdown(); + this.dismissDropdown(); + } } setAsDropdown() { -- cgit v1.2.1 From 625ecdbcc848f05eafaf167255901efd2d32cb37 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 16 Dec 2016 01:05:27 -0600 Subject: Create dropdown hint spec --- spec/features/issues/filter_issues_spec.rb | 682 --------------------- .../issues/filtered_search/dropdown_hint_spec.rb | 113 ++++ .../issues/filtered_search/filter_issues_spec.rb | 682 +++++++++++++++++++++ .../issues/filtered_search/search_bar_spec.rb | 86 +++ spec/features/issues/search_bar_spec.rb | 53 -- 5 files changed, 881 insertions(+), 735 deletions(-) delete mode 100644 spec/features/issues/filter_issues_spec.rb create mode 100644 spec/features/issues/filtered_search/dropdown_hint_spec.rb create mode 100644 spec/features/issues/filtered_search/filter_issues_spec.rb create mode 100644 spec/features/issues/filtered_search/search_bar_spec.rb delete mode 100644 spec/features/issues/search_bar_spec.rb diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb deleted file mode 100644 index 0b94bcc4e3f..00000000000 --- a/spec/features/issues/filter_issues_spec.rb +++ /dev/null @@ -1,682 +0,0 @@ -require 'rails_helper' - -describe 'Filter issues', feature: true do - include WaitForAjax - - let!(:group) { create(:group) } - let!(:project) { create(:project, group: group) } - let!(:user) { create(:user) } - let!(:user2) { create(:user) } - let!(:milestone) { create(:milestone, project: project) } - let!(:label) { create(:label, project: project) } - let!(:wontfix) { create(:label, project: project, title: "Won't fix") } - - let!(:bug_label) { create(:label, project: project, title: 'bug') } - let!(:caps_sensitive_label) { create(:label, project: project, title: 'CAPS_sensitive') } - let!(:milestone) { create(:milestone, title: "8", project: project) } - let!(:multiple_words_label) { create(:label, project: project, title: "Two words") } - - let!(:closed_issue) { create(:issue, title: 'bug that is closed', project: project, state: :closed) } - - def input_filtered_search(search_term) - filtered_search = find('.filtered-search') - filtered_search.set(search_term) - filtered_search.send_keys(:enter) - end - - def expect_filtered_search_input(input) - expect(find('.filtered-search').value).to eq(input) - end - - def expect_no_issues_list - page.within '.issues-list' do - expect(page).not_to have_selector('.issue') - end - end - - def expect_issues_list_count(open_count, closed_count = 0) - all_count = open_count + closed_count - - expect(page).to have_issuable_counts(open: open_count, closed: closed_count, all: all_count) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: open_count) - end - end - - before do - project.team << [user, :master] - project.team << [user2, :master] - group.add_developer(user) - group.add_developer(user2) - login_as(user) - create(:issue, project: project) - - create(:issue, title: "Bug report 1", project: project) - create(:issue, title: "Bug report 2", project: project) - create(:issue, title: "issue with 'single quotes'", project: project) - create(:issue, title: "issue with \"double quotes\"", project: project) - create(:issue, title: "issue with !@\#{$%^&*()-+", project: project) - create(:issue, title: "issue by assignee", project: project, milestone: milestone, author: user, assignee: user) - create(:issue, title: "issue by assignee with searchTerm", project: project, milestone: milestone, author: user, assignee: user) - - issue = create(:issue, - title: "Bug 2", - project: project, - milestone: milestone, - author: user, - assignee: user) - issue.labels << bug_label - - issue_with_caps_label = create(:issue, - title: "issue by assignee with searchTerm and label", - project: project, - milestone: milestone, - author: user, - assignee: user) - issue_with_caps_label.labels << caps_sensitive_label - - issue_with_everything = create(:issue, - title: "Bug report with everything you thought was possible", - project: project, - milestone: milestone, - author: user, - assignee: user) - issue_with_everything.labels << bug_label - issue_with_everything.labels << caps_sensitive_label - - multiple_words_label_issue = create(:issue, title: "Issue with multiple words label", project: project) - multiple_words_label_issue.labels << multiple_words_label - - future_milestone = create(:milestone, title: "future", project: project, due_date: Time.now + 1.month) - - create(:issue, - title: "Issue with future milestone", - milestone: future_milestone, - project: project) - - visit namespace_project_issues_path(project.namespace, project) - end - - describe 'filter issues by author' do - context 'only author', js: true do - it 'filters issues by searched author' do - input_filtered_search("author:@#{user.username}") - expect_issues_list_count(5) - end - - it 'filters issues by invalid author' do - # YOLO - end - - it 'filters issues by multiple authors' do - # YOLO - end - end - - context 'author with other filters', js: true do - it 'filters issues by searched author and text' do - search = "author:@#{user.username} issue" - input_filtered_search(search) - expect_issues_list_count(3) - expect_filtered_search_input(search) - end - - it 'filters issues by searched author, assignee and text' do - search = "author:@#{user.username} assignee:@#{user.username} issue" - input_filtered_search(search) - expect_issues_list_count(3) - expect_filtered_search_input(search) - end - - it 'filters issues by searched author, assignee, label, and text' do - search = "author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} issue" - input_filtered_search(search) - expect_issues_list_count(1) - expect_filtered_search_input(search) - end - - it 'filters issues by searched author, assignee, label, milestone and text' do - search = "author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} issue" - input_filtered_search(search) - expect_issues_list_count(1) - expect_filtered_search_input(search) - end - end - - context 'sorting', js: true do - # TODO - end - end - - describe 'filter issues by assignee' do - context 'only assignee', js: true do - it 'filters issues by searched assignee' do - search = "assignee:@#{user.username}" - input_filtered_search(search) - expect_issues_list_count(5) - expect_filtered_search_input(search) - end - - it 'filters issues by no assignee' do - search = "assignee:none" - input_filtered_search(search) - expect_issues_list_count(8, 1) - expect_filtered_search_input(search) - end - - it 'filters issues by invalid assignee' do - # YOLO - end - - it 'filters issues by multiple assignees' do - # YOLO - end - end - - context 'assignee with other filters', js: true do - it 'filters issues by searched assignee and text' do - search = "assignee:@#{user.username} searchTerm" - input_filtered_search(search) - expect_issues_list_count(2) - expect_filtered_search_input(search) - end - - it 'filters issues by searched assignee, author and text' do - search = "assignee:@#{user.username} author:@#{user.username} searchTerm" - input_filtered_search(search) - expect_issues_list_count(2) - expect_filtered_search_input(search) - end - - it 'filters issues by searched assignee, author, label, text' do - search = "assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} searchTerm" - input_filtered_search(search) - expect_issues_list_count(1) - expect_filtered_search_input(search) - end - - it 'filters issues by searched assignee, author, label, milestone and text' do - search = "assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} searchTerm" - input_filtered_search(search) - expect_issues_list_count(1) - expect_filtered_search_input(search) - end - end - - context 'sorting', js: true do - # TODO - end - end - - describe 'filter issues by label' do - context 'only label', js: true do - it 'filters issues by searched label' do - search = "label:~#{bug_label.title}" - input_filtered_search(search) - expect_issues_list_count(2) - expect_filtered_search_input(search) - end - - it 'filters issues by no label' do - search = "label:none" - input_filtered_search(search) - expect_issues_list_count(9, 1) - expect_filtered_search_input(search) - end - - it 'filters issues by invalid label' do - # YOLO - end - - it 'filters issues by multiple labels' do - search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title}" - input_filtered_search(search) - expect_issues_list_count(1) - expect_filtered_search_input(search) - end - - it 'filters issues by label containing special characters' do - special_label = create(:label, project: project, title: '!@#{$%^&*()-+[]<>?/:{}|\}') - special_issue = create(:issue, title: "Issue with special character label", project: project) - special_issue.labels << special_label - - search = "label:~#{special_label.title}" - input_filtered_search(search) - expect_issues_list_count(1) - expect_filtered_search_input(search) - end - - it 'does not show issues' do - new_label = create(:label, project: project, title: "new_label") - - search = "label:~#{new_label.title}" - input_filtered_search(search) - expect_no_issues_list() - expect_filtered_search_input(search) - end - end - - context 'label with multiple words', js: true do - it 'special characters' do - special_multiple_label = create(:label, project: project, title: "Utmost |mp0rt@nce") - special_multiple_issue = create(:issue, title: "Issue with special character multiple words label", project: project) - special_multiple_issue.labels << special_multiple_label - - search = "label:~'#{special_multiple_label.title}'" - input_filtered_search(search) - expect_issues_list_count(1) - - # filtered search defaults quotations to double quotes - expect_filtered_search_input("label:~\"#{special_multiple_label.title}\"") - end - - it 'single quotes' do - search = "label:~'#{multiple_words_label.title}'" - input_filtered_search(search) - expect_issues_list_count(1) - - expect_filtered_search_input("label:~\"#{multiple_words_label.title}\"") - end - - it 'double quotes' do - search = "label:~\"#{multiple_words_label.title}\"" - input_filtered_search(search) - expect_issues_list_count(1) - expect_filtered_search_input(search) - end - - it 'single quotes containing double quotes' do - double_quotes_label = create(:label, project: project, title: 'won"t fix') - double_quotes_label_issue = create(:issue, title: "Issue with double quotes label", project: project) - double_quotes_label_issue.labels << double_quotes_label - - search = "label:~'#{double_quotes_label.title}'" - input_filtered_search(search) - expect_issues_list_count(1) - expect_filtered_search_input(search) - end - - it 'double quotes containing single quotes' do - single_quotes_label = create(:label, project: project, title: "won't fix") - single_quotes_label_issue = create(:issue, title: "Issue with single quotes label", project: project) - single_quotes_label_issue.labels << single_quotes_label - - search = "label:~\"#{single_quotes_label.title}\"" - input_filtered_search(search) - expect_issues_list_count(1) - expect_filtered_search_input(search) - end - end - - context 'label with other filters', js: true do - it 'filters issues by searched label and text' do - search = "label:~#{caps_sensitive_label.title} bug" - input_filtered_search(search) - expect_issues_list_count(1) - expect_filtered_search_input(search) - end - - it 'filters issues by searched label, author and text' do - search = "label:~#{caps_sensitive_label.title} author:@#{user.username} bug" - input_filtered_search(search) - expect_issues_list_count(1) - expect_filtered_search_input(search) - end - - it 'filters issues by searched label, author, assignee and text' do - search = "label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug" - input_filtered_search(search) - expect_issues_list_count(1) - expect_filtered_search_input(search) - end - - it 'filters issues by searched label, author, assignee, milestone and text' do - search = "label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug" - input_filtered_search(search) - expect_issues_list_count(1) - expect_filtered_search_input(search) - end - end - - context 'multiple labels with other filters', js: true do - it 'filters issues by searched label, label2, and text' do - search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} bug" - input_filtered_search(search) - expect_issues_list_count(1) - expect_filtered_search_input(search) - end - - it 'filters issues by searched label, label2, author and text' do - search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} bug" - input_filtered_search(search) - expect_issues_list_count(1) - expect_filtered_search_input(search) - end - - it 'filters issues by searched label, label2, author, assignee and text' do - search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug" - input_filtered_search(search) - expect_issues_list_count(1) - expect_filtered_search_input(search) - end - - it 'filters issues by searched label, label2, author, assignee, milestone and text' do - search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug" - input_filtered_search(search) - expect_issues_list_count(1) - expect_filtered_search_input(search) - end - end - - context 'issue label clicked', js: true do - before do - find('.issues-list .issue .issue-info a .label', text: multiple_words_label.title).click - sleep 1 - end - - it 'filters' do - expect_issues_list_count(1) - end - - it 'displays in search bar' do - expect(find('.filtered-search').value).to eq("label:~\"#{multiple_words_label.title}\"") - end - end - - context 'sorting', js: true do - # TODO - end - end - - describe 'filter issues by milestone' do - context 'only milestone', js: true do - it 'filters issues by searched milestone' do - input_filtered_search("milestone:%#{milestone.title}") - expect_issues_list_count(5) - end - - it 'filters issues by no milestone' do - input_filtered_search("milestone:none") - expect_issues_list_count(7, 1) - end - - it 'filters issues by upcoming milestones' do - input_filtered_search("milestone:upcoming") - expect_issues_list_count(1) - end - - it 'filters issues by invalid milestones' do - # YOLO - end - - it 'filters issues by multiple milestones' do - # YOLO - end - - it 'filters issues by milestone containing special characters' do - special_milestone = create(:milestone, title: '!@\#{$%^&*()}', project: project) - create(:issue, title: "Issue with special character milestone", project: project, milestone: special_milestone) - - search = "milestone:%#{special_milestone.title}" - input_filtered_search(search) - expect_issues_list_count(1) - expect_filtered_search_input(search) - end - - it 'does not show issues' do - new_milestone = create(:milestone, title: "new", project: project) - - search = "milestone:%#{new_milestone.title}" - input_filtered_search(search) - expect_no_issues_list() - expect_filtered_search_input(search) - end - end - - context 'milestone with other filters', js: true do - it 'filters issues by searched milestone and text' do - search = "milestone:%#{milestone.title} bug" - input_filtered_search(search) - expect_issues_list_count(2) - expect_filtered_search_input(search) - end - - it 'filters issues by searched milestone, author and text' do - search = "milestone:%#{milestone.title} author:@#{user.username} bug" - input_filtered_search(search) - expect_issues_list_count(2) - expect_filtered_search_input(search) - end - - it 'filters issues by searched milestone, author, assignee and text' do - search = "milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} bug" - input_filtered_search(search) - expect_issues_list_count(2) - expect_filtered_search_input(search) - end - - it 'filters issues by searched milestone, author, assignee, label and text' do - search = "milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug" - input_filtered_search(search) - expect_issues_list_count(2) - expect_filtered_search_input(search) - end - end - - context 'sorting', js: true do - # TODO - end - end - - describe 'filter issues by text' do - context 'only text', js: true do - it 'filters issues by searched text' do - search = 'Bug' - input_filtered_search(search) - expect_issues_list_count(4, 1) - expect_filtered_search_input(search) - end - - it 'filters issues by multiple searched text' do - search = 'Bug report' - input_filtered_search(search) - expect_issues_list_count(3) - expect_filtered_search_input(search) - end - - it 'filters issues by case insensitive searched text' do - search = 'bug report' - input_filtered_search(search) - expect_issues_list_count(3) - expect_filtered_search_input(search) - end - - it 'filters issues by searched text containing single quotes' do - search = '\'single quotes\'' - input_filtered_search(search) - expect_issues_list_count(1) - expect_filtered_search_input(search) - end - - it 'filters issues by searched text containing double quotes' do - search = '"double quotes"' - input_filtered_search(search) - expect_issues_list_count(1) - expect_filtered_search_input(search) - end - - it 'filters issues by searched text containing special characters' do - search = '!@#{$%^&*()-+' - input_filtered_search(search) - expect_issues_list_count(1) - expect_filtered_search_input(search) - end - - it 'does not show any issues' do - search = 'testing' - input_filtered_search(search) - expect_no_issues_list() - expect_filtered_search_input(search) - end - end - - context 'searched text with other filters', js: true do - it 'filters issues by searched text and author' do - input_filtered_search("bug author:@#{user.username}") - expect_issues_list_count(2) - expect_filtered_search_input("author:@#{user.username} bug") - end - - it 'filters issues by searched text, author and more text' do - input_filtered_search("bug author:@#{user.username} report") - expect_issues_list_count(1) - expect_filtered_search_input("author:@#{user.username} bug report") - end - - it 'filters issues by searched text, author and assignee' do - input_filtered_search("bug author:@#{user.username} assignee:@#{user.username}") - expect_issues_list_count(2) - expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug") - end - - it 'filters issues by searched text, author, more text and assignee' do - input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username}") - expect_issues_list_count(1) - expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug report") - end - - it 'filters issues by searched text, author, more text, assignee and even more text' do - input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with") - expect_issues_list_count(1) - expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug report with") - end - - it 'filters issues by searched text, author, assignee and label' do - input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title}") - expect_issues_list_count(2) - expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug") - end - - it 'filters issues by searched text, author, text, assignee, text, label and text' do - input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything") - expect_issues_list_count(1) - expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug report with everything") - end - - it 'filters issues by searched text, author, assignee, label and milestone' do - input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title}") - expect_issues_list_count(2) - expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title} bug") - end - - it 'filters issues by searched text, author, text, assignee, text, label, text, milestone and text' do - input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything milestone:%#{milestone.title} you") - expect_issues_list_count(1) - expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title} bug report with everything you") - end - - it 'filters issues by searched text, author, assignee, multiple labels and milestone' do - input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title}") - expect_issues_list_count(1) - expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} bug") - end - - it 'filters issues by searched text, author, text, assignee, text, label1, text, label2, text, milestone and text' do - input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything label:~#{caps_sensitive_label.title} you milestone:%#{milestone.title} thought") - expect_issues_list_count(1) - expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} bug report with everything you thought") - end - end - - context 'sorting', js: true do - it 'sorts by oldest updated' do - create(:issue, - title: '3 days ago', - project: project, - author: user, - created_at: 3.days.ago, - updated_at: 3.days.ago) - - old_issue = create(:issue, - title: '5 days ago', - project: project, - author: user, - created_at: 5.days.ago, - updated_at: 5.days.ago) - - input_filtered_search('days ago') - expect_issues_list_count(2) - - sort_toggle = find('.filtered-search-container .dropdown-toggle') - sort_toggle.click - - find('.filtered-search-container .dropdown-menu li a', text: 'Oldest updated').click - wait_for_ajax - - expect(find('.issues-list .issue:first-of-type .issue-title-text a')).to have_content(old_issue.title) - end - end - end - - describe 'retains filter when switching issue states', js: true do - before do - input_filtered_search('bug') - - # Wait for search results to load - sleep 2 - end - - it 'open state' do - find('.issues-state-filters a', text: 'Closed').click - wait_for_ajax - - find('.issues-state-filters a', text: 'Open').click - wait_for_ajax - - expect(page).to have_selector('.issues-list .issue', count: 4) - end - - it 'closed state' do - find('.issues-state-filters a', text: 'Closed').click - wait_for_ajax - expect(page).to have_selector('.issues-list .issue', count: 1) - expect(find('.issues-list .issue:first-of-type .issue-title-text a')).to have_content(closed_issue.title) - end - - it 'all state' do - find('.issues-state-filters a', text: 'All').click - wait_for_ajax - expect(page).to have_selector('.issues-list .issue', count: 5) - end - end - - describe 'RSS feeds' do - it 'updates atom feed link for project issues' do - visit namespace_project_issues_path(project.namespace, project, milestone_title: milestone.title, assignee_id: user.id) - link = find('.nav-controls a', text: 'Subscribe') - params = CGI.parse(URI.parse(link[:href]).query) - auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) - auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query) - expect(params).to include('private_token' => [user.private_token]) - expect(params).to include('milestone_title' => [milestone.title]) - expect(params).to include('assignee_id' => [user.id.to_s]) - expect(auto_discovery_params).to include('private_token' => [user.private_token]) - expect(auto_discovery_params).to include('milestone_title' => [milestone.title]) - expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s]) - end - - it 'updates atom feed link for group issues' do - visit issues_group_path(group, milestone_title: milestone.title, assignee_id: user.id) - link = find('.nav-controls a', text: 'Subscribe') - params = CGI.parse(URI.parse(link[:href]).query) - auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) - auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query) - expect(params).to include('private_token' => [user.private_token]) - expect(params).to include('milestone_title' => [milestone.title]) - expect(params).to include('assignee_id' => [user.id.to_s]) - expect(auto_discovery_params).to include('private_token' => [user.private_token]) - expect(auto_discovery_params).to include('milestone_title' => [milestone.title]) - expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s]) - end - end -end diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb new file mode 100644 index 00000000000..364d4bf4db1 --- /dev/null +++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb @@ -0,0 +1,113 @@ +require 'rails_helper' + +describe 'Dropdown hint', js: true, feature: true do + include WaitForAjax + + let!(:project) { create(:empty_project) } + let!(:user) { create(:user) } + let(:filtered_search) { find('.filtered-search') } + before do + project.team << [user, :master] + login_as(user) + create(:issue, project: project) + + visit namespace_project_issues_path(project.namespace, project) + end + + describe 'behavior' do + before do + expect(page).to have_css('#js-dropdown-hint', visible: false) + filtered_search.click(); + end + + it 'opens when the search bar is first focused' do + expect(page).to have_css('#js-dropdown-hint', visible: true) + end + + it 'closes when the search bar is unfocused' do + find('body').click(); + expect(page).to have_css('#js-dropdown-hint', visible: false) + end + end + + describe 'filtering' do + it 'does not filter `Keep typing and press Enter`' do + filtered_search.set('randomtext') + expect(page).to have_css('#js-dropdown-hint', text: 'Keep typing and press Enter', visible: false) + expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(0) + end + + it 'filters with text' do + filtered_search.set('a') + expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(3) + end + end + + describe 'selecting from dropdown with no input' do + before do + filtered_search.click + end + + it 'opens the author dropdown when you click on author' do + find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'author').click + expect(page).to have_css('#js-dropdown-hint', visible: false) + expect(page).to have_css('#js-dropdown-author', visible: true) + expect(filtered_search.value).to eq('author:') + end + + it 'opens the assignee dropdown when you click on assignee' do + find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'assignee').click + expect(page).to have_css('#js-dropdown-hint', visible: false) + expect(page).to have_css('#js-dropdown-assignee', visible: true) + expect(filtered_search.value).to eq('assignee:') + end + + it 'opens the milestone dropdown when you click on milestone' do + find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'milestone').click + expect(page).to have_css('#js-dropdown-hint', visible: false) + expect(page).to have_css('#js-dropdown-milestone', visible: true) + expect(filtered_search.value).to eq('milestone:') + end + + it 'opens the label dropdown when you click on label' do + find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'label').click + expect(page).to have_css('#js-dropdown-hint', visible: false) + expect(page).to have_css('#js-dropdown-label', visible: true) + expect(filtered_search.value).to eq('label:') + end + end + + describe 'selecting from dropdown with some input' do + it 'opens the author dropdown when you click on author' do + filtered_search.set('auth') + find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'author').click + expect(page).to have_css('#js-dropdown-hint', visible: false) + expect(page).to have_css('#js-dropdown-author', visible: true) + expect(filtered_search.value).to eq('author:') + end + + it 'opens the assignee dropdown when you click on assignee' do + filtered_search.set('assign') + find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'assignee').click + expect(page).to have_css('#js-dropdown-hint', visible: false) + expect(page).to have_css('#js-dropdown-assignee', visible: true) + expect(filtered_search.value).to eq('assignee:') + end + + it 'opens the milestone dropdown when you click on milestone' do + filtered_search.set('mile') + find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'milestone').click + expect(page).to have_css('#js-dropdown-hint', visible: false) + expect(page).to have_css('#js-dropdown-milestone', visible: true) + expect(filtered_search.value).to eq('milestone:') + end + + it 'opens the label dropdown when you click on label' do + filtered_search.set('lab') + find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'label').click + expect(page).to have_css('#js-dropdown-hint', visible: false) + expect(page).to have_css('#js-dropdown-label', visible: true) + expect(filtered_search.value).to eq('label:') + end + end +end diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb new file mode 100644 index 00000000000..283814d2cbb --- /dev/null +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -0,0 +1,682 @@ +require 'rails_helper' + +describe 'Filter issues', feature: true do + include WaitForAjax + + let!(:group) { create(:group) } + let!(:project) { create(:project, group: group) } + let!(:user) { create(:user) } + let!(:user2) { create(:user) } + let!(:milestone) { create(:milestone, project: project) } + let!(:label) { create(:label, project: project) } + let!(:wontfix) { create(:label, project: project, title: "Won't fix") } + + let!(:bug_label) { create(:label, project: project, title: 'bug') } + let!(:caps_sensitive_label) { create(:label, project: project, title: 'CAPS_sensitive') } + let!(:milestone) { create(:milestone, title: "8", project: project) } + let!(:multiple_words_label) { create(:label, project: project, title: "Two words") } + + let!(:closed_issue) { create(:issue, title: 'bug that is closed', project: project, state: :closed) } + + def input_filtered_search(search_term) + filtered_search = find('.filtered-search') + filtered_search.set(search_term) + filtered_search.send_keys(:enter) + end + + def expect_filtered_search_input(input) + expect(find('.filtered-search').value).to eq(input) + end + + def expect_no_issues_list + page.within '.issues-list' do + expect(page).not_to have_selector('.issue') + end + end + + def expect_issues_list_count(open_count, closed_count = 0) + all_count = open_count + closed_count + + expect(page).to have_issuable_counts(open: open_count, closed: closed_count, all: all_count) + page.within '.issues-list' do + expect(page).to have_selector('.issue', count: open_count) + end + end + + before do + project.team << [user, :master] + project.team << [user2, :master] + group.add_developer(user) + group.add_developer(user2) + login_as(user) + create(:issue, project: project) + + create(:issue, title: "Bug report 1", project: project) + create(:issue, title: "Bug report 2", project: project) + create(:issue, title: "issue with 'single quotes'", project: project) + create(:issue, title: "issue with \"double quotes\"", project: project) + create(:issue, title: "issue with !@\#{$%^&*()-+", project: project) + create(:issue, title: "issue by assignee", project: project, milestone: milestone, author: user, assignee: user) + create(:issue, title: "issue by assignee with searchTerm", project: project, milestone: milestone, author: user, assignee: user) + + issue = create(:issue, + title: "Bug 2", + project: project, + milestone: milestone, + author: user, + assignee: user) + issue.labels << bug_label + + issue_with_caps_label = create(:issue, + title: "issue by assignee with searchTerm and label", + project: project, + milestone: milestone, + author: user, + assignee: user) + issue_with_caps_label.labels << caps_sensitive_label + + issue_with_everything = create(:issue, + title: "Bug report with everything you thought was possible", + project: project, + milestone: milestone, + author: user, + assignee: user) + issue_with_everything.labels << bug_label + issue_with_everything.labels << caps_sensitive_label + + multiple_words_label_issue = create(:issue, title: "Issue with multiple words label", project: project) + multiple_words_label_issue.labels << multiple_words_label + + future_milestone = create(:milestone, title: "future", project: project, due_date: Time.now + 1.month) + + create(:issue, + title: "Issue with future milestone", + milestone: future_milestone, + project: project) + + visit namespace_project_issues_path(project.namespace, project) + end + + describe 'filter issues by author' do + context 'only author', js: true do + it 'filters issues by searched author' do + input_filtered_search("author:@#{user.username}") + expect_issues_list_count(5) + end + + it 'filters issues by invalid author' do + # YOLO + end + + it 'filters issues by multiple authors' do + # YOLO + end + end + + context 'author with other filters', js: true do + it 'filters issues by searched author and text' do + search = "author:@#{user.username} issue" + input_filtered_search(search) + expect_issues_list_count(3) + expect_filtered_search_input(search) + end + + it 'filters issues by searched author, assignee and text' do + search = "author:@#{user.username} assignee:@#{user.username} issue" + input_filtered_search(search) + expect_issues_list_count(3) + expect_filtered_search_input(search) + end + + it 'filters issues by searched author, assignee, label, and text' do + search = "author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} issue" + input_filtered_search(search) + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'filters issues by searched author, assignee, label, milestone and text' do + search = "author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} issue" + input_filtered_search(search) + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + end + + context 'sorting', js: true do + # TODO + end + end + + describe 'filter issues by assignee' do + context 'only assignee', js: true do + it 'filters issues by searched assignee' do + search = "assignee:@#{user.username}" + input_filtered_search(search) + expect_issues_list_count(5) + expect_filtered_search_input(search) + end + + it 'filters issues by no assignee' do + search = "assignee:none" + input_filtered_search(search) + expect_issues_list_count(8, 1) + expect_filtered_search_input(search) + end + + it 'filters issues by invalid assignee' do + # YOLO + end + + it 'filters issues by multiple assignees' do + # YOLO + end + end + + context 'assignee with other filters', js: true do + it 'filters issues by searched assignee and text' do + search = "assignee:@#{user.username} searchTerm" + input_filtered_search(search) + expect_issues_list_count(2) + expect_filtered_search_input(search) + end + + it 'filters issues by searched assignee, author and text' do + search = "assignee:@#{user.username} author:@#{user.username} searchTerm" + input_filtered_search(search) + expect_issues_list_count(2) + expect_filtered_search_input(search) + end + + it 'filters issues by searched assignee, author, label, text' do + search = "assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} searchTerm" + input_filtered_search(search) + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'filters issues by searched assignee, author, label, milestone and text' do + search = "assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} searchTerm" + input_filtered_search(search) + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + end + + context 'sorting', js: true do + # TODO + end + end + + describe 'filter issues by label' do + context 'only label', js: true do + it 'filters issues by searched label' do + search = "label:~#{bug_label.title}" + input_filtered_search(search) + expect_issues_list_count(2) + expect_filtered_search_input(search) + end + + it 'filters issues by no label' do + search = "label:none" + input_filtered_search(search) + expect_issues_list_count(9, 1) + expect_filtered_search_input(search) + end + + it 'filters issues by invalid label' do + # YOLO + end + + it 'filters issues by multiple labels' do + search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title}" + input_filtered_search(search) + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'filters issues by label containing special characters' do + special_label = create(:label, project: project, title: '!@#{$%^&*()-+[]<>?/:{}|\}') + special_issue = create(:issue, title: "Issue with special character label", project: project) + special_issue.labels << special_label + + search = "label:~#{special_label.title}" + input_filtered_search(search) + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'does not show issues' do + new_label = create(:label, project: project, title: "new_label") + + search = "label:~#{new_label.title}" + input_filtered_search(search) + expect_no_issues_list() + expect_filtered_search_input(search) + end + end + + context 'label with multiple words', js: true do + it 'special characters' do + special_multiple_label = create(:label, project: project, title: "Utmost |mp0rt@nce") + special_multiple_issue = create(:issue, title: "Issue with special character multiple words label", project: project) + special_multiple_issue.labels << special_multiple_label + + search = "label:~'#{special_multiple_label.title}'" + input_filtered_search(search) + expect_issues_list_count(1) + + # filtered search defaults quotations to double quotes + expect_filtered_search_input("label:~\"#{special_multiple_label.title}\"") + end + + it 'single quotes' do + search = "label:~'#{multiple_words_label.title}'" + input_filtered_search(search) + expect_issues_list_count(1) + + expect_filtered_search_input("label:~\"#{multiple_words_label.title}\"") + end + + it 'double quotes' do + search = "label:~\"#{multiple_words_label.title}\"" + input_filtered_search(search) + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'single quotes containing double quotes' do + double_quotes_label = create(:label, project: project, title: 'won"t fix') + double_quotes_label_issue = create(:issue, title: "Issue with double quotes label", project: project) + double_quotes_label_issue.labels << double_quotes_label + + search = "label:~'#{double_quotes_label.title}'" + input_filtered_search(search) + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'double quotes containing single quotes' do + single_quotes_label = create(:label, project: project, title: "won't fix") + single_quotes_label_issue = create(:issue, title: "Issue with single quotes label", project: project) + single_quotes_label_issue.labels << single_quotes_label + + search = "label:~\"#{single_quotes_label.title}\"" + input_filtered_search(search) + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + end + + context 'label with other filters', js: true do + it 'filters issues by searched label and text' do + search = "label:~#{caps_sensitive_label.title} bug" + input_filtered_search(search) + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'filters issues by searched label, author and text' do + search = "label:~#{caps_sensitive_label.title} author:@#{user.username} bug" + input_filtered_search(search) + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'filters issues by searched label, author, assignee and text' do + search = "label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug" + input_filtered_search(search) + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'filters issues by searched label, author, assignee, milestone and text' do + search = "label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug" + input_filtered_search(search) + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + end + + context 'multiple labels with other filters', js: true do + it 'filters issues by searched label, label2, and text' do + search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} bug" + input_filtered_search(search) + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'filters issues by searched label, label2, author and text' do + search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} bug" + input_filtered_search(search) + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'filters issues by searched label, label2, author, assignee and text' do + search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug" + input_filtered_search(search) + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'filters issues by searched label, label2, author, assignee, milestone and text' do + search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug" + input_filtered_search(search) + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + end + + context 'issue label clicked', js: true do + before do + find('.issues-list .issue .issue-info a .label', text: multiple_words_label.title).click + sleep 1 + end + + it 'filters' do + expect_issues_list_count(1) + end + + it 'displays in search bar' do + expect(find('.filtered-search').value).to eq("label:~\"#{multiple_words_label.title}\"") + end + end + + context 'sorting', js: true do + # TODO + end + end + + describe 'filter issues by milestone' do + context 'only milestone', js: true do + it 'filters issues by searched milestone' do + input_filtered_search("milestone:%#{milestone.title}") + expect_issues_list_count(5) + end + + it 'filters issues by no milestone' do + input_filtered_search("milestone:none") + expect_issues_list_count(7, 1) + end + + it 'filters issues by upcoming milestones' do + input_filtered_search("milestone:upcoming") + expect_issues_list_count(1) + end + + it 'filters issues by invalid milestones' do + # YOLO + end + + it 'filters issues by multiple milestones' do + # YOLO + end + + it 'filters issues by milestone containing special characters' do + special_milestone = create(:milestone, title: '!@\#{$%^&*()}', project: project) + create(:issue, title: "Issue with special character milestone", project: project, milestone: special_milestone) + + search = "milestone:%#{special_milestone.title}" + input_filtered_search(search) + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'does not show issues' do + new_milestone = create(:milestone, title: "new", project: project) + + search = "milestone:%#{new_milestone.title}" + input_filtered_search(search) + expect_no_issues_list() + expect_filtered_search_input(search) + end + end + + context 'milestone with other filters', js: true do + it 'filters issues by searched milestone and text' do + search = "milestone:%#{milestone.title} bug" + input_filtered_search(search) + expect_issues_list_count(2) + expect_filtered_search_input(search) + end + + it 'filters issues by searched milestone, author and text' do + search = "milestone:%#{milestone.title} author:@#{user.username} bug" + input_filtered_search(search) + expect_issues_list_count(2) + expect_filtered_search_input(search) + end + + it 'filters issues by searched milestone, author, assignee and text' do + search = "milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} bug" + input_filtered_search(search) + expect_issues_list_count(2) + expect_filtered_search_input(search) + end + + it 'filters issues by searched milestone, author, assignee, label and text' do + search = "milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug" + input_filtered_search(search) + expect_issues_list_count(2) + expect_filtered_search_input(search) + end + end + + context 'sorting', js: true do + # TODO + end + end + + describe 'filter issues by text' do + context 'only text', js: true do + it 'filters issues by searched text' do + search = 'Bug' + input_filtered_search(search) + expect_issues_list_count(4, 1) + expect_filtered_search_input(search) + end + + it 'filters issues by multiple searched text' do + search = 'Bug report' + input_filtered_search(search) + expect_issues_list_count(3) + expect_filtered_search_input(search) + end + + it 'filters issues by case insensitive searched text' do + search = 'bug report' + input_filtered_search(search) + expect_issues_list_count(3) + expect_filtered_search_input(search) + end + + it 'filters issues by searched text containing single quotes' do + search = '\'single quotes\'' + input_filtered_search(search) + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'filters issues by searched text containing double quotes' do + search = '"double quotes"' + input_filtered_search(search) + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'filters issues by searched text containing special characters' do + search = '!@#{$%^&*()-+' + input_filtered_search(search) + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'does not show any issues' do + search = 'testing' + input_filtered_search(search) + expect_no_issues_list() + expect_filtered_search_input(search) + end + end + + context 'searched text with other filters', js: true do + it 'filters issues by searched text and author' do + input_filtered_search("bug author:@#{user.username}") + expect_issues_list_count(2) + expect_filtered_search_input("author:@#{user.username} bug") + end + + it 'filters issues by searched text, author and more text' do + input_filtered_search("bug author:@#{user.username} report") + expect_issues_list_count(1) + expect_filtered_search_input("author:@#{user.username} bug report") + end + + it 'filters issues by searched text, author and assignee' do + input_filtered_search("bug author:@#{user.username} assignee:@#{user.username}") + expect_issues_list_count(2) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug") + end + + it 'filters issues by searched text, author, more text and assignee' do + input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username}") + expect_issues_list_count(1) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug report") + end + + it 'filters issues by searched text, author, more text, assignee and even more text' do + input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with") + expect_issues_list_count(1) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug report with") + end + + it 'filters issues by searched text, author, assignee and label' do + input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title}") + expect_issues_list_count(2) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug") + end + + it 'filters issues by searched text, author, text, assignee, text, label and text' do + input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything") + expect_issues_list_count(1) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug report with everything") + end + + it 'filters issues by searched text, author, assignee, label and milestone' do + input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title}") + expect_issues_list_count(2) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title} bug") + end + + it 'filters issues by searched text, author, text, assignee, text, label, text, milestone and text' do + input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything milestone:%#{milestone.title} you") + expect_issues_list_count(1) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title} bug report with everything you") + end + + it 'filters issues by searched text, author, assignee, multiple labels and milestone' do + input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title}") + expect_issues_list_count(1) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} bug") + end + + it 'filters issues by searched text, author, text, assignee, text, label1, text, label2, text, milestone and text' do + input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything label:~#{caps_sensitive_label.title} you milestone:%#{milestone.title} thought") + expect_issues_list_count(1) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} bug report with everything you thought") + end + end + + context 'sorting', js: true do + it 'sorts by oldest updated' do + create(:issue, + title: '3 days ago', + project: project, + author: user, + created_at: 3.days.ago, + updated_at: 3.days.ago) + + old_issue = create(:issue, + title: '5 days ago', + project: project, + author: user, + created_at: 5.days.ago, + updated_at: 5.days.ago) + + input_filtered_search('days ago') + expect_issues_list_count(2) + + sort_toggle = find('.filtered-search-container .dropdown-toggle') + sort_toggle.click + + find('.filtered-search-container .dropdown-menu li a', text: 'Oldest updated').click + wait_for_ajax + + expect(find('.issues-list .issue:first-of-type .issue-title-text a')).to have_content(old_issue.title) + end + end + end + + describe 'retains filter when switching issue states', js: true do + before do + input_filtered_search('bug') + + # Wait for search results to load + sleep 2 + end + + it 'open state' do + find('.issues-state-filters a', text: 'Closed').click + wait_for_ajax + + find('.issues-state-filters a', text: 'Open').click + wait_for_ajax + + expect(page).to have_selector('.issues-list .issue', count: 4) + end + + it 'closed state' do + find('.issues-state-filters a', text: 'Closed').click + wait_for_ajax + expect(page).to have_selector('.issues-list .issue', count: 1) + expect(find('.issues-list .issue:first-of-type .issue-title-text a')).to have_content(closed_issue.title) + end + + it 'all state' do + find('.issues-state-filters a', text: 'All').click + wait_for_ajax + expect(page).to have_selector('.issues-list .issue', count: 5) + end + end + + describe 'RSS feeds' do + it 'updates atom feed link for project issues' do + visit namespace_project_issues_path(project.namespace, project, milestone_title: milestone.title, assignee_id: user.id) + link = find('.nav-controls a', text: 'Subscribe') + params = CGI.parse(URI.parse(link[:href]).query) + auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) + auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query) + expect(params).to include('private_token' => [user.private_token]) + expect(params).to include('milestone_title' => [milestone.title]) + expect(params).to include('assignee_id' => [user.id.to_s]) + expect(auto_discovery_params).to include('private_token' => [user.private_token]) + expect(auto_discovery_params).to include('milestone_title' => [milestone.title]) + expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s]) + end + + it 'updates atom feed link for group issues' do + visit issues_group_path(group, milestone_title: milestone.title, assignee_id: user.id) + link = find('.nav-controls a', text: 'Subscribe') + params = CGI.parse(URI.parse(link[:href]).query) + auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) + auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query) + expect(params).to include('private_token' => [user.private_token]) + expect(params).to include('milestone_title' => [milestone.title]) + expect(params).to include('assignee_id' => [user.id.to_s]) + expect(auto_discovery_params).to include('private_token' => [user.private_token]) + expect(auto_discovery_params).to include('milestone_title' => [milestone.title]) + expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s]) + end + end +end diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb new file mode 100644 index 00000000000..5862214cdc3 --- /dev/null +++ b/spec/features/issues/filtered_search/search_bar_spec.rb @@ -0,0 +1,86 @@ +require 'rails_helper' + +describe 'Search bar', js: true, feature: true do + include WaitForAjax + + let!(:project) { create(:empty_project) } + let!(:user) { create(:user) } + + before do + project.team << [user, :master] + login_as(user) + create(:issue, project: project) + + visit namespace_project_issues_path(project.namespace, project) + end + + def getLeftStyle(style) + leftStyle = /left:\s\d*[.]\d*px/.match(style) + leftStyle.to_s.gsub('left: ', '').to_f; + end + + describe 'clear search button' do + it 'clears text' do + search_text = 'search_text' + filtered_search = find('.filtered-search') + filtered_search.set(search_text) + + expect(filtered_search.value).to eq(search_text) + find('.filtered-search-input-container .clear-search').click + expect(filtered_search.value).to eq('') + end + + it 'hides by default' do + expect(page).to have_css('.clear-search', visible: false) + end + + it 'hides after clicked' do + filtered_search = find('.filtered-search') + filtered_search.set('a') + find('.filtered-search-input-container .clear-search').click + expect(page).to have_css('.clear-search', visible: false) + end + + it 'hides when there is no text' do + filtered_search = find('.filtered-search') + filtered_search.set('a') + filtered_search.set('') + expect(page).to have_css('.clear-search', visible: false) + end + + it 'shows when there is text' do + filtered_search = find('.filtered-search') + filtered_search.set('a') + + expect(page).to have_css('.clear-search', visible: true) + end + + it 'resets the dropdown hint filter' do + filtered_search = find('.filtered-search') + filtered_search.click(); + original_size = page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size + + filtered_search.set('author') + expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(1) + + find('.filtered-search-input-container .clear-search').click + filtered_search.click() + expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(original_size) + end + + it 'resets the dropdown filters' do + filtered_search = find('.filtered-search') + filtered_search.set('a') + hintStyle = page.find('#js-dropdown-hint')['style'] + hintOffset = getLeftStyle(hintStyle) + + filtered_search.set('author:') + expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(0) + + find('.filtered-search-input-container .clear-search').click + filtered_search.click() + expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to be > 0 + expect(getLeftStyle(page.find('#js-dropdown-hint')['style'])).to eq (hintOffset) + end + end +end diff --git a/spec/features/issues/search_bar_spec.rb b/spec/features/issues/search_bar_spec.rb deleted file mode 100644 index d0abdc284ea..00000000000 --- a/spec/features/issues/search_bar_spec.rb +++ /dev/null @@ -1,53 +0,0 @@ -require 'rails_helper' - -describe 'Search bar', js: true, feature: true do - include WaitForAjax - - let!(:project) { create(:empty_project) } - let!(:user) { create(:user) } - - before do - project.team << [user, :master] - login_as(user) - create(:issue, project: project) - - visit namespace_project_issues_path(project.namespace, project) - end - - describe 'clear search button' do - it 'clears text' do - search_text = 'search_text' - filtered_search = find('.filtered-search') - filtered_search.set(search_text) - - expect(filtered_search.value).to eq(search_text) - find('.filtered-search-input-container .clear-search').click - expect(filtered_search.value).to eq('') - end - - it 'hides by default' do - expect(page).to have_css('.clear-search', visible: false) - end - - it 'hides after clicked' do - filtered_search = find('.filtered-search') - filtered_search.set('a') - find('.filtered-search-input-container .clear-search').click - expect(page).to have_css('.clear-search', visible: false) - end - - it 'hides when there is no text' do - filtered_search = find('.filtered-search') - filtered_search.set('a') - filtered_search.set('') - expect(page).to have_css('.clear-search', visible: false) - end - - it 'shows when there is text' do - filtered_search = find('.filtered-search') - filtered_search.set('a') - - expect(page).to have_css('.clear-search', visible: true) - end - end -end -- cgit v1.2.1 From fd90a99639f5685aa86440aba1da6165b623e877 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 16 Dec 2016 01:08:07 -0600 Subject: Define filtered_search as a variable --- spec/features/issues/filtered_search/dropdown_hint_spec.rb | 1 + spec/features/issues/filtered_search/search_bar_spec.rb | 7 +------ 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb index 364d4bf4db1..216cd78850b 100644 --- a/spec/features/issues/filtered_search/dropdown_hint_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb @@ -6,6 +6,7 @@ describe 'Dropdown hint', js: true, feature: true do let!(:project) { create(:empty_project) } let!(:user) { create(:user) } let(:filtered_search) { find('.filtered-search') } + before do project.team << [user, :master] login_as(user) diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb index 5862214cdc3..d37057a44f8 100644 --- a/spec/features/issues/filtered_search/search_bar_spec.rb +++ b/spec/features/issues/filtered_search/search_bar_spec.rb @@ -5,6 +5,7 @@ describe 'Search bar', js: true, feature: true do let!(:project) { create(:empty_project) } let!(:user) { create(:user) } + let(:filtered_search) { find('.filtered-search') } before do project.team << [user, :master] @@ -22,7 +23,6 @@ describe 'Search bar', js: true, feature: true do describe 'clear search button' do it 'clears text' do search_text = 'search_text' - filtered_search = find('.filtered-search') filtered_search.set(search_text) expect(filtered_search.value).to eq(search_text) @@ -35,28 +35,24 @@ describe 'Search bar', js: true, feature: true do end it 'hides after clicked' do - filtered_search = find('.filtered-search') filtered_search.set('a') find('.filtered-search-input-container .clear-search').click expect(page).to have_css('.clear-search', visible: false) end it 'hides when there is no text' do - filtered_search = find('.filtered-search') filtered_search.set('a') filtered_search.set('') expect(page).to have_css('.clear-search', visible: false) end it 'shows when there is text' do - filtered_search = find('.filtered-search') filtered_search.set('a') expect(page).to have_css('.clear-search', visible: true) end it 'resets the dropdown hint filter' do - filtered_search = find('.filtered-search') filtered_search.click(); original_size = page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size @@ -69,7 +65,6 @@ describe 'Search bar', js: true, feature: true do end it 'resets the dropdown filters' do - filtered_search = find('.filtered-search') filtered_search.set('a') hintStyle = page.find('#js-dropdown-hint')['style'] hintOffset = getLeftStyle(hintStyle) -- cgit v1.2.1 From 9c4868141273b536cc0bc7fb80a662789fe89286 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 16 Dec 2016 10:22:09 -0600 Subject: Make changes to make it more flexible for new filters --- app/assets/javascripts/filtered_search/dropdown_hint.js.es6 | 2 +- .../filtered_search/filtered_search_manager.js.es6 | 12 +++++++++--- .../filtered_search/filtered_search_token_keys.js.es6 | 10 +++++++++- app/assets/stylesheets/framework/filters.scss | 4 ++++ 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index bdcece61984..7bf30143d78 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -37,7 +37,7 @@ if (selected.tagName === 'LI') { if (selected.hasAttribute('data-value')) { - this.dismissDropdown(); + this.dismissDropdown(); } else { const token = selected.querySelector('.js-filter-hint').innerText.trim(); const tag = selected.querySelector('.js-filter-tag').innerText.trim(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index e5b37f1e691..565f2347072 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -101,7 +101,8 @@ const match = gl.FilteredSearchTokenKeys.searchByKeyParam(keyParam); if (match) { - const sanitizedKey = keyParam.slice(0, keyParam.indexOf('_')); + const indexOf = keyParam.indexOf('_'); + const sanitizedKey = indexOf !== -1 ? keyParam.slice(0, keyParam.indexOf('_')) : keyParam; const symbol = match.symbol; let quotationsToUse = ''; @@ -137,14 +138,19 @@ const { param } = gl.FilteredSearchTokenKeys.searchByKey(token.key); let tokenPath = ''; + let keyParam = token.key; + if (param) { + keyParam += `_${param}`; + } + if (token.wildcard && condition) { tokenPath = condition.url; } else if (token.wildcard) { // wildcard means that the token does not have a symbol - tokenPath = `${token.key}_${param}=${encodeURIComponent(token.value)}`; + tokenPath = `${keyParam}=${encodeURIComponent(token.value)}`; } else { // Remove the token symbol - tokenPath = `${token.key}_${param}=${encodeURIComponent(token.value.slice(1))}`; + tokenPath = `${keyParam}=${encodeURIComponent(token.value.slice(1))}`; } paths.push(tokenPath); diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 index 6bd9cb06362..e46373024b6 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 @@ -57,7 +57,15 @@ } static searchByKeyParam(keyParam) { - return tokenKeys.find(tokenKey => keyParam === `${tokenKey.key}_${tokenKey.param}`) || null; + return tokenKeys.find((tokenKey) => { + let tokenKeyParam = tokenKey.key; + + if (tokenKey.param) { + tokenKeyParam += `_${tokenKey.param}`; + } + + return keyParam === tokenKeyParam; + }) || null; } static searchByConditionUrl(url) { diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 8b7cb245420..fee38b05023 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -89,6 +89,10 @@ overflow-y: hidden; border-radius: 0; + .fa { + width: 15px; + } + .dropdown-label-box { border-color: $white-light; border-style: solid; -- cgit v1.2.1 From e05d6b1cfac01c713016f20efae6962c3249e951 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 16 Dec 2016 16:25:52 -0600 Subject: Add specs for remaining dropdowns --- .../filtered_search/dropdown_assignee_spec.rb | 148 ++++++++++++++ .../issues/filtered_search/dropdown_author_spec.rb | 141 +++++++++++++ .../issues/filtered_search/dropdown_hint_spec.rb | 57 +++--- .../issues/filtered_search/dropdown_label_spec.rb | 211 ++++++++++++++++++++ .../filtered_search/dropdown_milestone_spec.rb | 222 +++++++++++++++++++++ .../issues/filtered_search/filter_issues_spec.rb | 40 ++-- .../issues/filtered_search/search_bar_spec.rb | 14 +- 7 files changed, 782 insertions(+), 51 deletions(-) create mode 100644 spec/features/issues/filtered_search/dropdown_assignee_spec.rb create mode 100644 spec/features/issues/filtered_search/dropdown_author_spec.rb create mode 100644 spec/features/issues/filtered_search/dropdown_label_spec.rb create mode 100644 spec/features/issues/filtered_search/dropdown_milestone_spec.rb diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb new file mode 100644 index 00000000000..5d0e95580c9 --- /dev/null +++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb @@ -0,0 +1,148 @@ +require 'rails_helper' + +describe 'Dropdown assignee', js: true, feature: true do + include WaitForAjax + + let!(:project) { create(:empty_project) } + let!(:user) { create(:user, name: 'administrator', username: 'root') } + let!(:user_john) { create(:user, name: 'John', username: 'th0mas') } + let!(:user_jacob) { create(:user, name: 'Jacob', username: 'otter32') } + let(:filtered_search) { find('.filtered-search') } + let(:js_dropdown_assignee) { '#js-dropdown-assignee' } + + def send_keys_to_filtered_search(input) + input.split("").each do |i| + filtered_search.send_keys(i) + sleep 3 + wait_for_ajax + end + end + + def dropdown_assignee_size + page.all('#js-dropdown-assignee .filter-dropdown .filter-dropdown-item').size + end + + def click_assignee(text) + find('#js-dropdown-assignee .filter-dropdown .filter-dropdown-item', text: text).click + end + + before do + project.team << [user, :master] + project.team << [user_john, :master] + project.team << [user_jacob, :master] + login_as(user) + create(:issue, project: project) + + visit namespace_project_issues_path(project.namespace, project) + end + + describe 'behavior' do + it 'opens when the search bar has assignee:' do + filtered_search.set('assignee:') + expect(page).to have_css(js_dropdown_assignee, visible: true) + end + + it 'closes when the search bar is unfocused' do + find('body').click() + expect(page).to have_css(js_dropdown_assignee, visible: false) + end + + it 'should show loading indicator when opened' do + filtered_search.set('assignee:') + expect(page).to have_css('#js-dropdown-assignee .filter-dropdown-loading', visible: true) + end + + it 'should hide loading indicator when loaded' do + send_keys_to_filtered_search('assignee:') + expect(page).not_to have_css('#js-dropdown-assignee .filter-dropdown-loading') + end + + it 'should load all the assignees when opened' do + send_keys_to_filtered_search('assignee:') + expect(dropdown_assignee_size).to eq(3) + end + end + + describe 'filtering' do + before do + filtered_search.set('assignee:') + end + + it 'filters by name' do + send_keys_to_filtered_search('j') + expect(dropdown_assignee_size).to eq(2) + end + + it 'filters by case insensitive name' do + send_keys_to_filtered_search('J') + expect(dropdown_assignee_size).to eq(2) + end + + it 'filters by username with symbol' do + send_keys_to_filtered_search('@ot') + expect(dropdown_assignee_size).to eq(2) + end + + it 'filters by case insensitive username with symbol' do + send_keys_to_filtered_search('@OT') + expect(dropdown_assignee_size).to eq(2) + end + + it 'filters by username without symbol' do + send_keys_to_filtered_search('ot') + expect(dropdown_assignee_size).to eq(2) + end + + it 'filters by case insensitive username without symbol' do + send_keys_to_filtered_search('OT') + expect(dropdown_assignee_size).to eq(2) + end + end + + describe 'selecting from dropdown' do + before do + filtered_search.set('assignee:') + end + + it 'fills in the assignee username when the assignee has not been filtered' do + click_assignee(user_jacob.name) + expect(page).to have_css(js_dropdown_assignee, visible: false) + expect(filtered_search.value).to eq("assignee:@#{user_jacob.username}") + end + + it 'fills in the assignee username when the assignee has been filtered' do + send_keys_to_filtered_search('roo') + click_assignee(user.name) + expect(page).to have_css(js_dropdown_assignee, visible: false) + expect(filtered_search.value).to eq("assignee:@#{user.username}") + end + + it 'selects `no assignee`' do + click_assignee('No Assignee') + expect(page).to have_css(js_dropdown_assignee, visible: false) + expect(filtered_search.value).to eq("assignee:none") + end + end + + describe 'input has existing content' do + it 'opens assignee dropdown with existing search term' do + filtered_search.set('searchTerm assignee:') + expect(page).to have_css(js_dropdown_assignee, visible: true) + end + + it 'opens assignee dropdown with existing author' do + filtered_search.set('author:@user assignee:') + expect(page).to have_css(js_dropdown_assignee, visible: true) + end + + it 'opens assignee dropdown with existing label' do + filtered_search.set('label:~bug assignee:') + expect(page).to have_css(js_dropdown_assignee, visible: true) + end + + it 'opens assignee dropdown with existing milestone' do + filtered_search.set('milestone:%v1.0 assignee:') + expect(page).to have_css(js_dropdown_assignee, visible: true) + end + end +end diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb new file mode 100644 index 00000000000..c7cb5e25174 --- /dev/null +++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb @@ -0,0 +1,141 @@ +require 'rails_helper' + +describe 'Dropdown author', js: true, feature: true do + include WaitForAjax + + let!(:project) { create(:empty_project) } + let!(:user) { create(:user, name: 'administrator', username: 'root') } + let!(:user_john) { create(:user, name: 'John', username: 'th0mas') } + let!(:user_jacob) { create(:user, name: 'Jacob', username: 'otter32') } + let(:filtered_search) { find('.filtered-search') } + let(:js_dropdown_author) { '#js-dropdown-author' } + + def send_keys_to_filtered_search(input) + input.split("").each do |i| + filtered_search.send_keys(i) + sleep 3 + wait_for_ajax + end + end + + def dropdown_author_size + page.all('#js-dropdown-author .filter-dropdown .filter-dropdown-item').size + end + + def click_author(text) + find('#js-dropdown-author .filter-dropdown .filter-dropdown-item', text: text).click + end + + before do + project.team << [user, :master] + project.team << [user_john, :master] + project.team << [user_jacob, :master] + login_as(user) + create(:issue, project: project) + + visit namespace_project_issues_path(project.namespace, project) + end + + describe 'behavior' do + it 'opens when the search bar has author:' do + filtered_search.set('author:') + expect(page).to have_css(js_dropdown_author, visible: true) + end + + it 'closes when the search bar is unfocused' do + find('body').click() + expect(page).to have_css(js_dropdown_author, visible: false) + end + + it 'should show loading indicator when opened' do + filtered_search.set('author:') + expect(page).to have_css('#js-dropdown-author .filter-dropdown-loading', visible: true) + end + + it 'should hide loading indicator when loaded' do + send_keys_to_filtered_search('author:') + expect(page).not_to have_css('#js-dropdown-author .filter-dropdown-loading') + end + + it 'should load all the authors when opened' do + send_keys_to_filtered_search('author:') + expect(dropdown_author_size).to eq(3) + end + end + + describe 'filtering' do + before do + filtered_search.set('author:') + end + + it 'filters by name' do + send_keys_to_filtered_search('j') + expect(dropdown_author_size).to eq(2) + end + + it 'filters by case insensitive name' do + send_keys_to_filtered_search('J') + expect(dropdown_author_size).to eq(2) + end + + it 'filters by username with symbol' do + send_keys_to_filtered_search('@ot') + expect(dropdown_author_size).to eq(2) + end + + it 'filters by case insensitive username with symbol' do + send_keys_to_filtered_search('@OT') + expect(dropdown_author_size).to eq(2) + end + + it 'filters by username without symbol' do + send_keys_to_filtered_search('ot') + expect(dropdown_author_size).to eq(2) + end + + it 'filters by case insensitive username without symbol' do + send_keys_to_filtered_search('OT') + expect(dropdown_author_size).to eq(2) + end + end + + describe 'selecting from dropdown' do + before do + filtered_search.set('author:') + end + + it 'fills in the author username when the author has not been filtered' do + click_author(user_jacob.name) + expect(page).to have_css(js_dropdown_author, visible: false) + expect(filtered_search.value).to eq("author:@#{user_jacob.username}") + end + + it 'fills in the author username when the author has been filtered' do + click_author(user.name) + expect(page).to have_css(js_dropdown_author, visible: false) + expect(filtered_search.value).to eq("author:@#{user.username}") + end + end + + describe 'input has existing content' do + it 'opens author dropdown with existing search term' do + filtered_search.set('searchTerm author:') + expect(page).to have_css(js_dropdown_author, visible: true) + end + + it 'opens author dropdown with existing assignee' do + filtered_search.set('assignee:@user author:') + expect(page).to have_css(js_dropdown_author, visible: true) + end + + it 'opens author dropdown with existing label' do + filtered_search.set('label:~bug author:') + expect(page).to have_css(js_dropdown_author, visible: true) + end + + it 'opens author dropdown with existing milestone' do + filtered_search.set('milestone:%v1.0 author:') + expect(page).to have_css(js_dropdown_author, visible: true) + end + end +end diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb index 216cd78850b..c4328bc18b0 100644 --- a/spec/features/issues/filtered_search/dropdown_hint_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb @@ -6,6 +6,15 @@ describe 'Dropdown hint', js: true, feature: true do let!(:project) { create(:empty_project) } let!(:user) { create(:user) } let(:filtered_search) { find('.filtered-search') } + let(:js_dropdown_hint) { '#js-dropdown-hint' } + + def dropdown_hint_size + page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size + end + + def click_hint(text) + find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: text).click + end before do project.team << [user, :master] @@ -17,30 +26,30 @@ describe 'Dropdown hint', js: true, feature: true do describe 'behavior' do before do - expect(page).to have_css('#js-dropdown-hint', visible: false) - filtered_search.click(); + expect(page).to have_css(js_dropdown_hint, visible: false) + filtered_search.click() end it 'opens when the search bar is first focused' do - expect(page).to have_css('#js-dropdown-hint', visible: true) + expect(page).to have_css(js_dropdown_hint, visible: true) end it 'closes when the search bar is unfocused' do - find('body').click(); - expect(page).to have_css('#js-dropdown-hint', visible: false) + find('body').click() + expect(page).to have_css(js_dropdown_hint, visible: false) end end describe 'filtering' do it 'does not filter `Keep typing and press Enter`' do filtered_search.set('randomtext') - expect(page).to have_css('#js-dropdown-hint', text: 'Keep typing and press Enter', visible: false) - expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(0) + expect(page).to have_css(js_dropdown_hint, text: 'Keep typing and press Enter', visible: false) + expect(dropdown_hint_size).to eq(0) end it 'filters with text' do filtered_search.set('a') - expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(3) + expect(dropdown_hint_size).to eq(3) end end @@ -50,29 +59,29 @@ describe 'Dropdown hint', js: true, feature: true do end it 'opens the author dropdown when you click on author' do - find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'author').click - expect(page).to have_css('#js-dropdown-hint', visible: false) + click_hint('author') + expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-author', visible: true) expect(filtered_search.value).to eq('author:') end it 'opens the assignee dropdown when you click on assignee' do - find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'assignee').click - expect(page).to have_css('#js-dropdown-hint', visible: false) + click_hint('assignee') + expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-assignee', visible: true) expect(filtered_search.value).to eq('assignee:') end it 'opens the milestone dropdown when you click on milestone' do - find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'milestone').click - expect(page).to have_css('#js-dropdown-hint', visible: false) + click_hint('milestone') + expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-milestone', visible: true) expect(filtered_search.value).to eq('milestone:') end it 'opens the label dropdown when you click on label' do - find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'label').click - expect(page).to have_css('#js-dropdown-hint', visible: false) + click_hint('label') + expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-label', visible: true) expect(filtered_search.value).to eq('label:') end @@ -81,32 +90,32 @@ describe 'Dropdown hint', js: true, feature: true do describe 'selecting from dropdown with some input' do it 'opens the author dropdown when you click on author' do filtered_search.set('auth') - find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'author').click - expect(page).to have_css('#js-dropdown-hint', visible: false) + click_hint('author') + expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-author', visible: true) expect(filtered_search.value).to eq('author:') end it 'opens the assignee dropdown when you click on assignee' do filtered_search.set('assign') - find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'assignee').click - expect(page).to have_css('#js-dropdown-hint', visible: false) + click_hint('assignee') + expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-assignee', visible: true) expect(filtered_search.value).to eq('assignee:') end it 'opens the milestone dropdown when you click on milestone' do filtered_search.set('mile') - find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'milestone').click - expect(page).to have_css('#js-dropdown-hint', visible: false) + click_hint('milestone') + expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-milestone', visible: true) expect(filtered_search.value).to eq('milestone:') end it 'opens the label dropdown when you click on label' do filtered_search.set('lab') - find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'label').click - expect(page).to have_css('#js-dropdown-hint', visible: false) + click_hint('label') + expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-label', visible: true) expect(filtered_search.value).to eq('label:') end diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb new file mode 100644 index 00000000000..d007e160b3e --- /dev/null +++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb @@ -0,0 +1,211 @@ +require 'rails_helper' + +describe 'Dropdown label', js: true, feature: true do + include WaitForAjax + + let!(:project) { create(:empty_project) } + let!(:user) { create(:user) } + let!(:bug_label) { create(:label, project: project, title: 'bug') } + let!(:uppercase_label) { create(:label, project: project, title: 'BUG') } + let!(:two_words_label) { create(:label, project: project, title: 'High Priority') } + let!(:wont_fix_label) { create(:label, project: project, title: 'Won"t Fix') } + let!(:special_label) { create(:label, project: project, title: '!@#$%^+&*()')} + let!(:long_label) { create(:label, project: project, title: 'this is a very long title this is a very long title this is a very long title this is a very long title this is a very long title')} + let(:filtered_search) { find('.filtered-search') } + let(:js_dropdown_label) { '#js-dropdown-label' } + + def send_keys_to_filtered_search(input) + input.split("").each do |i| + filtered_search.send_keys(i) + sleep 3 + wait_for_ajax + sleep 3 + end + end + + def dropdown_label_size + page.all('#js-dropdown-label .filter-dropdown .filter-dropdown-item').size + end + + def click_label(text) + find('#js-dropdown-label .filter-dropdown .filter-dropdown-item', text: text).click + end + + before do + project.team << [user, :master] + login_as(user) + create(:issue, project: project) + + visit namespace_project_issues_path(project.namespace, project) + end + + describe 'behavior' do + it 'opens when the search bar has label:' do + filtered_search.set('label:') + expect(page).to have_css(js_dropdown_label, visible: true) + end + + it 'closes when the search bar is unfocused' do + find('body').click() + expect(page).to have_css(js_dropdown_label, visible: false) + end + + it 'should show loading indicator when opened' do + filtered_search.set('label:') + expect(page).to have_css('#js-dropdown-label .filter-dropdown-loading', visible: true) + end + + it 'should hide loading indicator when loaded' do + send_keys_to_filtered_search('label:') + expect(page).not_to have_css('#js-dropdown-label .filter-dropdown-loading') + end + + it 'should load all the labels when opened' do + send_keys_to_filtered_search('label:') + expect(dropdown_label_size).to be > 0 + end + end + + describe 'filtering' do + before do + filtered_search.set('label:') + end + + it 'filters by name' do + send_keys_to_filtered_search('b') + expect(dropdown_label_size).to eq(2) + end + + it 'filters by case insensitive name' do + send_keys_to_filtered_search('B') + expect(dropdown_label_size).to eq(2) + end + + it 'filters by name with symbol' do + send_keys_to_filtered_search('~bu') + expect(dropdown_label_size).to eq(2) + end + + it 'filters by case insensitive name with symbol' do + send_keys_to_filtered_search('~BU') + expect(dropdown_label_size).to eq(2) + end + + it 'filters by multiple names using double quotes' do + send_keys_to_filtered_search('"High P') + expect(dropdown_label_size).to eq(1) + end + + it 'filters by multiple names using single quotes' do + send_keys_to_filtered_search('\'High P') + expect(dropdown_label_size).to eq(1) + end + + it 'filters by multiple names using single and double quotes' do + send_keys_to_filtered_search('~"won`\'t f') + expect(dropdown_label_size).to eq(1) + end + + it 'filters by multiple names using double quotes with symbol' do + send_keys_to_filtered_search('~"High P') + expect(dropdown_label_size).to eq(1) + end + + it 'filters by multiple names using single quotes with symbol' do + send_keys_to_filtered_search('~\'High P') + expect(dropdown_label_size).to eq(1) + end + + it 'filters by special characters' do + send_keys_to_filtered_search('^+') + expect(dropdown_label_size).to eq(1) + end + + it 'filters by special characters with symbol' do + send_keys_to_filtered_search('~^+') + expect(dropdown_label_size).to eq(1) + end + end + + describe 'selecting from dropdown' do + before do + filtered_search.set('label:') + end + + it 'fills in the label name when the label has not been filled' do + click_label(bug_label.title) + expect(page).to have_css(js_dropdown_label, visible: false) + expect(filtered_search.value).to eq("label:~#{bug_label.title}") + end + + it 'fills in the label name when the label is partially filled' do + send_keys_to_filtered_search('bu') + click_label(bug_label.title) + expect(page).to have_css(js_dropdown_label, visible: false) + expect(filtered_search.value).to eq("label:~#{bug_label.title}") + end + + it 'fills in the label name that contains multiple words' do + click_label(two_words_label.title) + expect(page).to have_css(js_dropdown_label, visible: false) + expect(filtered_search.value).to eq("label:~\"#{two_words_label.title}\"") + end + + it 'fills in the label name that contains multiple words and is very long' do + click_label(long_label.title) + expect(page).to have_css(js_dropdown_label, visible: false) + expect(filtered_search.value).to eq("label:~\"#{long_label.title}\"") + end + + it 'fills in the label name that contains double quotes' do + click_label(wont_fix_label.title) + expect(page).to have_css(js_dropdown_label, visible: false) + expect(filtered_search.value).to eq("label:~'#{wont_fix_label.title}'") + end + + it 'fills in the label name with the correct capitalization' do + click_label(uppercase_label.title) + expect(page).to have_css(js_dropdown_label, visible: false) + expect(filtered_search.value).to eq("label:~#{uppercase_label.title}") + end + + it 'fills in the label name with special characters' do + click_label(special_label.title) + expect(page).to have_css(js_dropdown_label, visible: false) + expect(filtered_search.value).to eq("label:~#{special_label.title}") + end + + it 'selects `no label`' do + click_label('No Label') + expect(page).to have_css(js_dropdown_label, visible: false) + expect(filtered_search.value).to eq("label:none") + end + end + + describe 'input has existing content' do + it 'opens label dropdown with existing search term' do + filtered_search.set('searchTerm label:') + expect(page).to have_css(js_dropdown_label, visible: true) + end + + it 'opens label dropdown with existing author' do + filtered_search.set('author:@person label:') + expect(page).to have_css(js_dropdown_label, visible: true) + end + + it 'opens label dropdown with existing assignee' do + filtered_search.set('assignee:@person label:') + expect(page).to have_css(js_dropdown_label, visible: true) + end + + it 'opens label dropdown with existing label' do + filtered_search.set('label:~urgent label:') + expect(page).to have_css(js_dropdown_label, visible: true) + end + + it 'opens label dropdown with existing milestone' do + filtered_search.set('milestone:%v2.0 label:') + expect(page).to have_css(js_dropdown_label, visible: true) + end + end +end diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb new file mode 100644 index 00000000000..d367430a670 --- /dev/null +++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb @@ -0,0 +1,222 @@ +require 'rails_helper' + +describe 'Dropdown milestone', js: true, feature: true do + include WaitForAjax + + let!(:project) { create(:empty_project) } + let!(:user) { create(:user) } + let!(:milestone) { create(:milestone, title: 'v1.0', project: project) } + let!(:uppercase_milestone) { create(:milestone, title: 'CAP_MILESTONE', project: project) } + let!(:two_words_milestone) { create(:milestone, title: 'Future Plan', project: project) } + let!(:wont_fix_milestone) { create(:milestone, title: 'Won"t Fix', project: project) } + let!(:special_milestone) { create(:milestone, title: '!@#$%^&*(+)', project: project) } + let!(:long_milestone) { create(:milestone, title: 'this is a very long title this is a very long title this is a very long title this is a very long title this is a very long title', project: project) } + + let(:filtered_search) { find('.filtered-search') } + let(:js_dropdown_milestone) { '#js-dropdown-milestone' } + + def send_keys_to_filtered_search(input) + input.split("").each do |i| + filtered_search.send_keys(i) + sleep 3 + wait_for_ajax + sleep 3 + end + end + + def dropdown_milestone_size + page.all('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item').size + end + + def click_milestone(text) + find('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item', text: text).click + end + + def click_static_milestone(text) + find('#js-dropdown-milestone .filter-dropdown-item', text: text).click + end + + before do + project.team << [user, :master] + login_as(user) + create(:issue, project: project) + + visit namespace_project_issues_path(project.namespace, project) + end + + describe 'behavior' do + it 'opens when the search bar has milestone:' do + filtered_search.set('milestone:') + expect(page).to have_css(js_dropdown_milestone, visible: true) + end + + it 'closes when the search bar is unfocused' do + find('body').click() + expect(page).to have_css(js_dropdown_milestone, visible: false) + end + + it 'should show loading indicator when opened' do + filtered_search.set('milestone:') + expect(page).to have_css('#js-dropdown-milestone .filter-dropdown-loading', visible: true) + end + + it 'should hide loading indicator when loaded' do + send_keys_to_filtered_search('milestone:') + expect(page).not_to have_css('#js-dropdown-milestone .filter-dropdown-loading') + end + + it 'should load all the milestones when opened' do + send_keys_to_filtered_search('milestone:') + expect(dropdown_milestone_size).to be > 0 + end + end + + describe 'filtering' do + before do + filtered_search.set('milestone:') + end + + it 'filters by name' do + send_keys_to_filtered_search('v1') + expect(dropdown_milestone_size).to eq(1) + end + + it 'filters by case insensitive name' do + send_keys_to_filtered_search('V1') + expect(dropdown_milestone_size).to eq(1) + end + + it 'filters by name with symbol' do + send_keys_to_filtered_search('%v1') + expect(dropdown_milestone_size).to eq(1) + end + + it 'filters by case insensitive name with symbol' do + send_keys_to_filtered_search('%V1') + expect(dropdown_milestone_size).to eq(1) + end + + it 'filters by multiple names using double quotes' do + send_keys_to_filtered_search('"future') + expect(dropdown_milestone_size).to eq(1) + end + + it 'filters by multiple names using single quotes' do + send_keys_to_filtered_search('\'future p') + expect(dropdown_milestone_size).to eq(1) + end + + it 'filters by multiple names using single and double quotes' do + send_keys_to_filtered_search('%"won`\'t f') + expect(dropdown_milestone_size).to eq(1) + end + + it 'filters by multiple names using double quotes with symbol' do + send_keys_to_filtered_search('%"future p') + expect(dropdown_milestone_size).to eq(1) + end + + it 'filters by multiple names using single quotes with symbol' do + send_keys_to_filtered_search('%\'future p') + expect(dropdown_milestone_size).to eq(1) + end + + it 'filters by special characters' do + send_keys_to_filtered_search('^+') + expect(dropdown_milestone_size).to eq(1) + end + + it 'filters by special characters with symbol' do + send_keys_to_filtered_search('~^+') + expect(dropdown_milestone_size).to eq(1) + end + end + + describe 'selecting from dropdown' do + before do + filtered_search.set('milestone:') + end + + it 'fills in the milestone name when the milestone has not been filled' do + click_milestone(milestone.title) + expect(page).to have_css(js_dropdown_milestone, visible: false) + expect(filtered_search.value).to eq("milestone:%#{milestone.title}") + end + + it 'fills in the milestone name when the milestone is partially filled' do + send_keys_to_filtered_search('v') + click_milestone(milestone.title) + expect(page).to have_css(js_dropdown_milestone, visible: false) + expect(filtered_search.value).to eq("milestone:%#{milestone.title}") + end + + it 'fills in the milestone name that contains multiple words' do + click_milestone(two_words_milestone.title) + expect(page).to have_css(js_dropdown_milestone, visible: false) + expect(filtered_search.value).to eq("milestone:%\"#{two_words_milestone.title}\"") + end + + it 'fills in the milestone name that contains multiple words and is very long' do + click_milestone(long_milestone.title) + expect(page).to have_css(js_dropdown_milestone, visible: false) + expect(filtered_search.value).to eq("milestone:%\"#{long_milestone.title}\"") + end + + it 'fills in the milestone name that contains double quotes' do + click_milestone(wont_fix_milestone.title) + expect(page).to have_css(js_dropdown_milestone, visible: false) + expect(filtered_search.value).to eq("milestone:%'#{wont_fix_milestone.title}'") + end + + it 'fills in the milestone name with the correct capitalization' do + click_milestone(uppercase_milestone.title) + expect(page).to have_css(js_dropdown_milestone, visible: false) + expect(filtered_search.value).to eq("milestone:%#{uppercase_milestone.title}") + end + + it 'fills in the milestone name with special characters' do + click_milestone(special_milestone.title) + expect(page).to have_css(js_dropdown_milestone, visible: false) + expect(filtered_search.value).to eq("milestone:%#{special_milestone.title}") + end + + it 'selects `no milestone`' do + click_static_milestone('No Milestone') + expect(page).to have_css(js_dropdown_milestone, visible: false) + expect(filtered_search.value).to eq("milestone:none") + end + + it 'selects `upcoming milestone`' do + click_static_milestone('Upcoming') + expect(page).to have_css(js_dropdown_milestone, visible: false) + expect(filtered_search.value).to eq("milestone:upcoming") + end + end + + describe 'input has existing content' do + it 'opens milestone dropdown with existing search term' do + filtered_search.set('searchTerm milestone:') + expect(page).to have_css(js_dropdown_milestone, visible: true) + end + + it 'opens milestone dropdown with existing author' do + filtered_search.set('author:@john milestone:') + expect(page).to have_css(js_dropdown_milestone, visible: true) + end + + it 'opens milestone dropdown with existing assignee' do + filtered_search.set('assignee:@john milestone:') + expect(page).to have_css(js_dropdown_milestone, visible: true) + end + + it 'opens milestone dropdown with existing label' do + filtered_search.set('label:~important milestone:') + expect(page).to have_css(js_dropdown_milestone, visible: true) + end + + it 'opens milestone dropdown with existing milestone' do + filtered_search.set('milestone:%100 milestone:') + expect(page).to have_css(js_dropdown_milestone, visible: true) + end + end +end diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index 283814d2cbb..e72417aea80 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -describe 'Filter issues', feature: true do +describe 'Filter issues', js: true, feature: true do include WaitForAjax let!(:group) { create(:group) } @@ -98,7 +98,7 @@ describe 'Filter issues', feature: true do end describe 'filter issues by author' do - context 'only author', js: true do + context 'only author' do it 'filters issues by searched author' do input_filtered_search("author:@#{user.username}") expect_issues_list_count(5) @@ -113,7 +113,7 @@ describe 'Filter issues', feature: true do end end - context 'author with other filters', js: true do + context 'author with other filters' do it 'filters issues by searched author and text' do search = "author:@#{user.username} issue" input_filtered_search(search) @@ -143,13 +143,13 @@ describe 'Filter issues', feature: true do end end - context 'sorting', js: true do + context 'sorting' do # TODO end end describe 'filter issues by assignee' do - context 'only assignee', js: true do + context 'only assignee' do it 'filters issues by searched assignee' do search = "assignee:@#{user.username}" input_filtered_search(search) @@ -173,7 +173,7 @@ describe 'Filter issues', feature: true do end end - context 'assignee with other filters', js: true do + context 'assignee with other filters' do it 'filters issues by searched assignee and text' do search = "assignee:@#{user.username} searchTerm" input_filtered_search(search) @@ -203,13 +203,13 @@ describe 'Filter issues', feature: true do end end - context 'sorting', js: true do + context 'sorting' do # TODO end end describe 'filter issues by label' do - context 'only label', js: true do + context 'only label' do it 'filters issues by searched label' do search = "label:~#{bug_label.title}" input_filtered_search(search) @@ -256,7 +256,7 @@ describe 'Filter issues', feature: true do end end - context 'label with multiple words', js: true do + context 'label with multiple words' do it 'special characters' do special_multiple_label = create(:label, project: project, title: "Utmost |mp0rt@nce") special_multiple_issue = create(:issue, title: "Issue with special character multiple words label", project: project) @@ -308,7 +308,7 @@ describe 'Filter issues', feature: true do end end - context 'label with other filters', js: true do + context 'label with other filters' do it 'filters issues by searched label and text' do search = "label:~#{caps_sensitive_label.title} bug" input_filtered_search(search) @@ -338,7 +338,7 @@ describe 'Filter issues', feature: true do end end - context 'multiple labels with other filters', js: true do + context 'multiple labels with other filters' do it 'filters issues by searched label, label2, and text' do search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} bug" input_filtered_search(search) @@ -368,7 +368,7 @@ describe 'Filter issues', feature: true do end end - context 'issue label clicked', js: true do + context 'issue label clicked' do before do find('.issues-list .issue .issue-info a .label', text: multiple_words_label.title).click sleep 1 @@ -383,13 +383,13 @@ describe 'Filter issues', feature: true do end end - context 'sorting', js: true do + context 'sorting' do # TODO end end describe 'filter issues by milestone' do - context 'only milestone', js: true do + context 'only milestone' do it 'filters issues by searched milestone' do input_filtered_search("milestone:%#{milestone.title}") expect_issues_list_count(5) @@ -433,7 +433,7 @@ describe 'Filter issues', feature: true do end end - context 'milestone with other filters', js: true do + context 'milestone with other filters' do it 'filters issues by searched milestone and text' do search = "milestone:%#{milestone.title} bug" input_filtered_search(search) @@ -463,13 +463,13 @@ describe 'Filter issues', feature: true do end end - context 'sorting', js: true do + context 'sorting' do # TODO end end describe 'filter issues by text' do - context 'only text', js: true do + context 'only text' do it 'filters issues by searched text' do search = 'Bug' input_filtered_search(search) @@ -520,7 +520,7 @@ describe 'Filter issues', feature: true do end end - context 'searched text with other filters', js: true do + context 'searched text with other filters' do it 'filters issues by searched text and author' do input_filtered_search("bug author:@#{user.username}") expect_issues_list_count(2) @@ -588,7 +588,7 @@ describe 'Filter issues', feature: true do end end - context 'sorting', js: true do + context 'sorting' do it 'sorts by oldest updated' do create(:issue, title: '3 days ago', @@ -618,7 +618,7 @@ describe 'Filter issues', feature: true do end end - describe 'retains filter when switching issue states', js: true do + describe 'retains filter when switching issue states' do before do input_filtered_search('bug') diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb index d37057a44f8..3af36ae27dd 100644 --- a/spec/features/issues/filtered_search/search_bar_spec.rb +++ b/spec/features/issues/filtered_search/search_bar_spec.rb @@ -15,9 +15,9 @@ describe 'Search bar', js: true, feature: true do visit namespace_project_issues_path(project.namespace, project) end - def getLeftStyle(style) - leftStyle = /left:\s\d*[.]\d*px/.match(style) - leftStyle.to_s.gsub('left: ', '').to_f; + def get_left_style(style) + left_style = /left:\s\d*[.]\d*px/.match(style) + left_style.to_s.gsub('left: ', '').to_f end describe 'clear search button' do @@ -53,7 +53,7 @@ describe 'Search bar', js: true, feature: true do end it 'resets the dropdown hint filter' do - filtered_search.click(); + filtered_search.click() original_size = page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size filtered_search.set('author') @@ -66,8 +66,8 @@ describe 'Search bar', js: true, feature: true do it 'resets the dropdown filters' do filtered_search.set('a') - hintStyle = page.find('#js-dropdown-hint')['style'] - hintOffset = getLeftStyle(hintStyle) + hint_style = page.find('#js-dropdown-hint')['style'] + hint_offset = get_left_style(hint_style) filtered_search.set('author:') expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(0) @@ -75,7 +75,7 @@ describe 'Search bar', js: true, feature: true do find('.filtered-search-input-container .clear-search').click filtered_search.click() expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to be > 0 - expect(getLeftStyle(page.find('#js-dropdown-hint')['style'])).to eq (hintOffset) + expect(get_left_style(page.find('#js-dropdown-hint')['style'])).to eq hint_offset end end end -- cgit v1.2.1 From 0e40c952d6d715580ed0ec891dc6f4fdc810673e Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 16 Dec 2016 18:19:54 -0600 Subject: Remove unused finder variable --- app/views/shared/issuable/_search_bar.html.haml | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 896769768eb..3449c1f0151 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -1,6 +1,3 @@ -- finder = controller.controller_name == 'issues' || controller.controller_name == 'boards' ? issues_finder : merge_requests_finder -- boards_page = controller.controller_name == 'boards' - .issues-filters .issues-details-filters.row-content-block.second-block.filtered-search-block = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do -- cgit v1.2.1 From e197f27f19ab7995d280f67754ea16c2629701b2 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 17 Dec 2016 13:22:00 -0600 Subject: Refactor and use regex for string processing --- .../filtered_search/dropdown_hint.js.es6 | 58 ++-- .../filtered_search/dropdown_user.js.es6 | 9 +- .../filtered_search/dropdown_utils.js.es6 | 30 +- .../filtered_search_dropdown.js.es6 | 2 +- .../filtered_search_dropdown_manager.js.es6 | 34 +- .../filtered_search/filtered_search_manager.js.es6 | 14 +- .../filtered_search_tokenizer.js.es6 | 178 ++--------- .../filtered_search/dropdown_utils_spec.js.es6 | 25 +- .../filtered_search_dropdown_manager_spec.js.es6 | 22 +- .../filtered_search_tokenizer_spec.js.es6 | 353 ++++++--------------- 10 files changed, 189 insertions(+), 536 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index 7bf30143d78..c5ab9c52d76 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -3,31 +3,13 @@ /* global droplabFilter */ (() => { - const dropdownData = [{ - icon: 'fa-pencil', - hint: 'author:', - tag: '<author>', - }, { - icon: 'fa-user', - hint: 'assignee:', - tag: '<assignee>', - }, { - icon: 'fa-clock-o', - hint: 'milestone:', - tag: '<milestone>', - }, { - icon: 'fa-tag', - hint: 'label:', - tag: '<label>', - }]; - class DropdownHint extends gl.FilteredSearchDropdown { constructor(droplab, dropdown, input) { super(droplab, dropdown, input); this.config = { droplabFilter: { template: 'hint', - filterFunction: gl.DropdownUtils.filterMethod, + filterFunction: gl.DropdownUtils.filterHint, }, }; } @@ -43,8 +25,7 @@ const tag = selected.querySelector('.js-filter-tag').innerText.trim(); if (tag.length) { - gl.FilteredSearchDropdownManager - .addWordToInput(this.getSelectedTextWithoutEscaping(token)); + gl.FilteredSearchDropdownManager.addWordToInput(token); } this.dismissDropdown(); this.dispatchInputEvent(); @@ -52,24 +33,27 @@ } } - getSelectedTextWithoutEscaping(selectedToken) { - const lastWord = this.input.value.split(' ').last(); - const lastWordIndex = selectedToken.indexOf(lastWord); - - return lastWordIndex === -1 ? selectedToken : selectedToken.slice(lastWord.length); - } - renderContent() { - this.droplab.changeHookList(this.hookId, this.dropdown, [droplabFilter], this.config); - - // Clone dropdownData to prevent it from being - // changed due to pass by reference - const data = []; - dropdownData.forEach((item) => { - data.push(Object.assign({}, item)); - }); + const dropdownData = [{ + icon: 'fa-pencil', + hint: 'author:', + tag: '<author>', + }, { + icon: 'fa-user', + hint: 'assignee:', + tag: '<assignee>', + }, { + icon: 'fa-clock-o', + hint: 'milestone:', + tag: '<milestone>', + }, { + icon: 'fa-tag', + hint: 'label:', + tag: '<label>', + }]; - this.droplab.setData(this.hookId, data); + this.droplab.changeHookList(this.hookId, this.dropdown, [droplabFilter], this.config); + this.droplab.setData(this.hookId, dropdownData); } init() { diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 index 7a566907312..49581e3bfbd 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 @@ -37,13 +37,10 @@ } getSearchInput() { - const query = this.input.value; - const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); - const valueWithoutColon = value.slice(1); - const hasPrefix = valueWithoutColon[0] === '@'; - const valueWithoutPrefix = valueWithoutColon.slice(1); + const query = this.input.value.trim(); + const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); - return hasPrefix ? valueWithoutPrefix : valueWithoutColon; + return lastToken.value || ''; } init() { diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 index 3837b020fd3..d246000ff52 100644 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 @@ -22,30 +22,32 @@ static filterWithSymbol(filterSymbol, item, query) { const updatedItem = item; - const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); - const valueWithoutColon = value.slice(1).toLowerCase(); - const prefix = valueWithoutColon[0]; - const valueWithoutPrefix = valueWithoutColon.slice(1); + const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(query); - const title = updatedItem.title.toLowerCase(); + if (lastToken !== searchToken) { + const value = lastToken.value.toLowerCase(); + const title = updatedItem.title.toLowerCase(); - // Eg. filterSymbol = ~ for labels - const matchWithoutPrefix = - prefix === filterSymbol && title.indexOf(valueWithoutPrefix) !== -1; - const match = title.indexOf(valueWithoutColon) !== -1; + // Eg. filterSymbol = ~ for labels + const matchWithoutSymbol = lastToken.symbol === filterSymbol && title.indexOf(value) !== -1; + const match = title.indexOf(`${lastToken.symbol}${value}`) !== -1; + + updatedItem.droplab_hidden = !match && !matchWithoutSymbol; + } else { + updatedItem.droplab_hidden = false; + } - updatedItem.droplab_hidden = !match && !matchWithoutPrefix; return updatedItem; } - static filterMethod(item, query) { + static filterHint(item, query) { const updatedItem = item; - const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); + const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); - if (value === '') { + if (!lastToken) { updatedItem.droplab_hidden = false; } else { - updatedItem.droplab_hidden = updatedItem.hint.indexOf(value) === -1; + updatedItem.droplab_hidden = updatedItem.hint.indexOf(lastToken.toLowerCase()) === -1; } return updatedItem; diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 index 68014e27462..56147ad93c9 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -29,7 +29,7 @@ itemClicked(e, getValueFunction) { const { selected } = e.detail; - if (selected.tagName === 'LI') { + if (selected.tagName === 'LI' && selected.innerHTML) { const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(selected); if (!dataValueSet) { diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 index ac71b5e4434..b67176267fb 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 @@ -57,17 +57,25 @@ static addWordToInput(word, addSpace = false) { const input = document.querySelector('.filtered-search'); + input.value = input.value.trim(); + const value = input.value; const hasExistingValue = value.length !== 0; - const { lastToken } = gl.FilteredSearchTokenizer.processTokens(value); + const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(value); + + // Find out what part of the token value the user has typed + // and remove it from input before appending the selected token value + if (lastToken !== searchToken) { + const lastTokenString = `${lastToken.symbol}${lastToken.value}`; - if ({}.hasOwnProperty.call(lastToken, 'key')) { // Spaces inside the token means that the token value will be escaped by quotes - const hasQuotes = lastToken.value.indexOf(' ') !== -1; + const hasQuotes = lastTokenString.indexOf(' ') !== -1; // Add 2 length to account for the length of the front and back quotes - const lengthToRemove = hasQuotes ? lastToken.value.length + 2 : lastToken.value.length; + const lengthToRemove = hasQuotes ? lastTokenString.length + 2 : lastTokenString.length; input.value = value.slice(0, -1 * (lengthToRemove)); + } else if (searchToken !== '' && word.indexOf(searchToken) !== -1) { + input.value = value.slice(0, -1 * searchToken.length); } input.value += hasExistingValue && addSpace ? ` ${word}` : word; @@ -129,27 +137,25 @@ const match = gl.FilteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase()); const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key - && {}.hasOwnProperty.call(this.mapping, match.key); + && this.mapping[match.key]; const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint'; if (shouldOpenFilterDropdown || shouldOpenHintDropdown) { - // `hint` is not listed as a tokenKey (since it is not a real `filter`) - const key = match && {}.hasOwnProperty.call(match, 'key') ? match.key : 'hint'; + const key = match && match.key ? match.key : 'hint'; this.load(key, firstLoad); } - - gl.droplab = this.droplab; } setDropdown() { - const { lastToken } = this.tokenizer.processTokens(this.filteredSearchInput.value); + const { lastToken, searchToken } = this.tokenizer + .processTokens(this.filteredSearchInput.value); - if (typeof lastToken === 'string') { + if (lastToken === searchToken) { // Token is not fully initialized yet because it has no value // Eg. token = 'label:' - const { tokenKey } = this.tokenizer.parseToken(lastToken); - this.loadDropdown(tokenKey); - } else if ({}.hasOwnProperty.call(lastToken, 'key')) { + const split = lastToken.split(':'); + this.loadDropdown(split.length > 1 ? split[0] : ''); + } else if (lastToken) { // Token has been initialized into an object because it has a value this.loadDropdown(lastToken.key); } else { diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 565f2347072..d2ea4de18aa 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -136,21 +136,13 @@ const condition = gl.FilteredSearchTokenKeys .searchByConditionKeyValue(token.key, token.value.toLowerCase()); const { param } = gl.FilteredSearchTokenKeys.searchByKey(token.key); + const keyParam = param ? `${token.key}_${param}` : token.key; let tokenPath = ''; - let keyParam = token.key; - if (param) { - keyParam += `_${param}`; - } - - if (token.wildcard && condition) { + if (condition) { tokenPath = condition.url; - } else if (token.wildcard) { - // wildcard means that the token does not have a symbol - tokenPath = `${keyParam}=${encodeURIComponent(token.value)}`; } else { - // Remove the token symbol - tokenPath = `${keyParam}=${encodeURIComponent(token.value.slice(1))}`; + tokenPath = `${keyParam}=${encodeURIComponent(token.value)}`; } paths.push(tokenPath); diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 index 57c0e8fc359..14ca78e139b 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 @@ -1,165 +1,39 @@ (() => { class FilteredSearchTokenizer { - static parseToken(input) { - const colonIndex = input.indexOf(':'); - let tokenKey; - let tokenValue; - let tokenSymbol; - - if (colonIndex !== -1) { - tokenKey = input.slice(0, colonIndex).toLowerCase(); - tokenValue = input.slice(colonIndex + 1); - tokenSymbol = tokenValue[0]; - } - - return { - tokenKey, - tokenValue, - tokenSymbol, - }; - } - - static getLastTokenObject(input) { - const token = FilteredSearchTokenizer.getLastToken(input); - const colonIndex = token.indexOf(':'); - - const key = colonIndex !== -1 ? token.slice(0, colonIndex) : ''; - const value = colonIndex !== -1 ? token.slice(colonIndex) : token; - - return { - key, - value, - }; - } - - static getLastToken(input) { - let completeToken = false; - let completeQuotation = true; - let lastQuotation = ''; - let i = input.length; - - const doubleQuote = '"'; - const singleQuote = '\''; - while (!completeToken && i >= 0) { - const isDoubleQuote = input[i] === doubleQuote; - const isSingleQuote = input[i] === singleQuote; - - // If the second quotation is found - if ((lastQuotation === doubleQuote && isDoubleQuote) || - (lastQuotation === singleQuote && isSingleQuote)) { - completeQuotation = true; - } - - // Save the first quotation - if ((isDoubleQuote && lastQuotation === '') || - (isSingleQuote && lastQuotation === '')) { - lastQuotation = input[i]; - completeQuotation = false; - } - - if (completeQuotation && input[i] === ' ') { - completeToken = true; - } else { - i -= 1; - } - } - - // Adjust by 1 because of empty space - return input.slice(i + 1); - } - static processTokens(input) { + const tokenRegex = /(\w+):([~%@]?)(?:"(.*?)"|'(.*?)'|(\S+))/g; const tokens = []; - let searchToken = ''; - let lastToken = ''; - - const inputs = input.split(' '); - let searchTerms = ''; - let lastQuotation = ''; - let incompleteToken = false; - - // Iterate through each word (broken up by spaces) - inputs.forEach((i) => { - if (incompleteToken) { - // Continue previous token as it had an escaped - // quote in the beginning - const prevToken = tokens.last(); - prevToken.value += ` ${i}`; - - // Remove last quotation from the value - const lastQuotationRegex = new RegExp(lastQuotation, 'g'); - prevToken.value = prevToken.value.replace(lastQuotationRegex, ''); - tokens[tokens.length - 1] = prevToken; - - // Check to see if this quotation completes the token value - if (i.indexOf(lastQuotation) !== -1) { - lastToken = tokens.last(); - incompleteToken = !incompleteToken; - } - - return; - } - - const colonIndex = i.indexOf(':'); - - if (colonIndex !== -1) { - const { tokenKey, tokenValue, tokenSymbol } = gl.FilteredSearchTokenizer.parseToken(i); - - const keyMatch = gl.FilteredSearchTokenKeys.searchByKey(tokenKey); - const symbolMatch = gl.FilteredSearchTokenKeys.searchBySymbol(tokenSymbol); - - const doubleQuoteOccurrences = tokenValue.split('"').length - 1; - const singleQuoteOccurrences = tokenValue.split('\'').length - 1; - - const doubleQuoteIndex = tokenValue.indexOf('"'); - const singleQuoteIndex = tokenValue.indexOf('\''); - - const doubleQuoteExist = doubleQuoteIndex !== -1; - const singleQuoteExist = singleQuoteIndex !== -1; - - const doubleQuoteExistOnly = doubleQuoteExist && !singleQuoteExist; - const doubleQuoteIsBeforeSingleQuote = - doubleQuoteExist && singleQuoteExist && doubleQuoteIndex < singleQuoteIndex; - - const singleQuoteExistOnly = singleQuoteExist && !doubleQuoteExist; - const singleQuoteIsBeforeDoubleQuote = - doubleQuoteExist && singleQuoteExist && singleQuoteIndex < doubleQuoteIndex; - - if ((doubleQuoteExistOnly || doubleQuoteIsBeforeSingleQuote) - && doubleQuoteOccurrences % 2 !== 0) { - // " is found and is in front of ' (if any) - lastQuotation = '"'; - incompleteToken = true; - } else if ((singleQuoteExistOnly || singleQuoteIsBeforeDoubleQuote) - && singleQuoteOccurrences % 2 !== 0) { - // ' is found and is in front of " (if any) - lastQuotation = '\''; - incompleteToken = true; - } - - if (keyMatch && tokenValue.length > 0) { - tokens.push({ - key: keyMatch.key, - value: tokenValue, - wildcard: !symbolMatch, - }); - lastToken = tokens.last(); - - return; - } + let lastToken = null; + const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => { + let tokenValue = v1 || v2 || v3; + let tokenSymbol = symbol; + + if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') { + tokenSymbol = tokenValue; + tokenValue = ''; } - // Add space for next term - searchTerms += `${i} `; - lastToken = i; - }, this); - - searchToken = searchTerms.trim(); + tokens.push({ + key, + value: tokenValue || '', + symbol: tokenSymbol || '', + }); + return ''; + }).replace(/\s{2,}/g, ' ').trim() || ''; + + if (tokens.length > 0) { + const last = tokens[tokens.length - 1]; + const lastString = `${last.key}:${last.symbol}${last.value}`; + lastToken = input.lastIndexOf(lastString) === + input.length - lastString.length ? last : searchToken; + } else { + lastToken = searchToken; + } return { tokens, - searchToken, lastToken, + searchToken, }; } } diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 b/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 index 07293b9f877..369eb90e31c 100644 --- a/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 +++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 @@ -34,11 +34,6 @@ title: '@root', }; - beforeEach(() => { - spyOn(gl.FilteredSearchTokenizer, 'getLastTokenObject') - .and.callFake(query => ({ value: query })); - }); - it('should filter without symbol', () => { const updatedItem = gl.DropdownUtils.filterWithSymbol('@', item, ':roo'); expect(updatedItem.droplab_hidden).toBe(false); @@ -49,37 +44,27 @@ expect(updatedItem.droplab_hidden).toBe(false); }); - it('should filter with invalid symbol', () => { - const updatedItem = gl.DropdownUtils.filterWithSymbol('@', item, ':#'); - expect(updatedItem.droplab_hidden).toBe(true); - }); - it('should filter with colon', () => { const updatedItem = gl.DropdownUtils.filterWithSymbol('@', item, ':'); expect(updatedItem.droplab_hidden).toBe(false); }); }); - describe('filterMethod', () => { - beforeEach(() => { - spyOn(gl.FilteredSearchTokenizer, 'getLastTokenObject') - .and.callFake(query => ({ value: query })); - }); - - it('should filter by hint', () => { - let updatedItem = gl.DropdownUtils.filterMethod({ + describe('filterHint', () => { + it('should filter', () => { + let updatedItem = gl.DropdownUtils.filterHint({ hint: 'label', }, 'l'); expect(updatedItem.droplab_hidden).toBe(false); - updatedItem = gl.DropdownUtils.filterMethod({ + updatedItem = gl.DropdownUtils.filterHint({ hint: 'label', }, 'o'); expect(updatedItem.droplab_hidden).toBe(true); }); it('should return droplab_hidden false when item has no hint', () => { - const updatedItem = gl.DropdownUtils.filterMethod({}, ''); + const updatedItem = gl.DropdownUtils.filterHint({}, ''); expect(updatedItem.droplab_hidden).toBe(false); }); }); diff --git a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 index 17d414aaad1..562673a4ee5 100644 --- a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 +++ b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 @@ -21,13 +21,6 @@ }); describe('input has no existing value', () => { - beforeEach(() => { - spyOn(gl.FilteredSearchTokenizer, 'processTokens') - .and.callFake(() => ({ - lastToken: {}, - })); - }); - it('should add word', () => { gl.FilteredSearchDropdownManager.addWordToInput('firstWord'); expect(getInputValue()).toBe('firstWord'); @@ -61,26 +54,13 @@ value: 'roo', }; - spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.callFake(() => ({ - lastToken, - })); - document.querySelector('.filtered-search').value = `${lastToken.key}:${lastToken.value}`; gl.FilteredSearchDropdownManager.addWordToInput('root'); expect(getInputValue()).toBe('author:root'); }); it('should only add the remaining characters of the word (contains space)', () => { - const lastToken = { - key: 'label', - value: 'test me', - }; - - spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.callFake(() => ({ - lastToken, - })); - - document.querySelector('.filtered-search').value = `${lastToken.key}:"${lastToken.value}"`; + document.querySelector('.filtered-search').value = 'label:~"test'; gl.FilteredSearchDropdownManager.addWordToInput('~\'"test me"\''); expect(getInputValue()).toBe('label:~\'"test me"\''); }); diff --git a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 index c93f163e763..8e5992fa446 100644 --- a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 +++ b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 @@ -4,267 +4,100 @@ (() => { describe('Filtered Search Tokenizer', () => { - describe('parseToken', () => { - it('should return key, value and symbol', () => { - const { tokenKey, tokenValue, tokenSymbol } = gl.FilteredSearchTokenizer - .parseToken('author:@user'); - - expect(tokenKey).toBe('author'); - expect(tokenValue).toBe('@user'); - expect(tokenSymbol).toBe('@'); - }); - - it('should return value with spaces', () => { - const { tokenKey, tokenValue, tokenSymbol } = gl.FilteredSearchTokenizer - .parseToken('label:~"test me"'); - - expect(tokenKey).toBe('label'); - expect(tokenValue).toBe('~"test me"'); - expect(tokenSymbol).toBe('~'); - }); - }); - - describe('getLastTokenObject', () => { - beforeEach(() => { - spyOn(gl.FilteredSearchTokenizer, 'getLastToken').and.callFake(input => input); - }); - - it('should return key and value', () => { - const { key, value } = gl.FilteredSearchTokenizer.getLastTokenObject('author:@root'); - expect(key).toBe('author'); - expect(value).toBe(':@root'); - }); - - describe('string without colon', () => { - let lastTokenObject; - - beforeEach(() => { - lastTokenObject = gl.FilteredSearchTokenizer.getLastTokenObject('author'); - }); - - it('should return key as an empty string', () => { - expect(lastTokenObject.key).toBe(''); - }); - - it('should return input as value', () => { - expect(lastTokenObject.value).toBe('author'); - }); - }); - }); - - describe('getLastToken', () => { - it('returns entire string when there is only one word', () => { - const lastToken = gl.FilteredSearchTokenizer.getLastToken('input'); - expect(lastToken).toBe('input'); - }); - - it('returns last word when there are multiple words', () => { - const lastToken = gl.FilteredSearchTokenizer.getLastToken('this is a few words'); - expect(lastToken).toBe('words'); - }); - - it('returns last token when there are multiple tokens', () => { - const lastToken = gl.FilteredSearchTokenizer - .getLastToken('label:fun author:root milestone:2.0'); - expect(lastToken).toBe('milestone:2.0'); - }); - - it('returns last token containing spaces escaped by double quotes', () => { - const lastToken = gl.FilteredSearchTokenizer - .getLastToken('label:fun author:root milestone:2.0 label:~"Feature Proposal"'); - expect(lastToken).toBe('label:~"Feature Proposal"'); - }); - - it('returns last token containing spaces escaped by single quotes', () => { - const lastToken = gl.FilteredSearchTokenizer - .getLastToken('label:fun author:root milestone:2.0 label:~\'Feature Proposal\''); - expect(lastToken).toBe('label:~\'Feature Proposal\''); - }); - - it('returns last token containing special characters', () => { - const lastToken = gl.FilteredSearchTokenizer - .getLastToken('label:fun author:root milestone:2.0 label:~!@#$%^&*()'); - expect(lastToken).toBe('label:~!@#$%^&*()'); - }); - }); - describe('processTokens', () => { - describe('input does not contain any tokens', () => { - let results; - beforeEach(() => { - results = gl.FilteredSearchTokenizer.processTokens('searchTerm'); - }); - - it('returns input as searchToken', () => { - expect(results.searchToken).toBe('searchTerm'); - }); - - it('returns tokens as an empty array', () => { - expect(results.tokens.length).toBe(0); - }); - - it('returns lastToken equal to searchToken', () => { - expect(results.lastToken).toBe(results.searchToken); - }); - }); - - describe('input contains only tokens', () => { - let results; - beforeEach(() => { - results = gl.FilteredSearchTokenizer - .processTokens('author:@root label:~"Very Important" milestone:%v1.0 assignee:none'); - }); - - it('returns searchToken as an empty string', () => { - expect(results.searchToken).toBe(''); - }); - - it('returns tokens array of size equal to the number of tokens in input', () => { - expect(results.tokens.length).toBe(4); - }); - - it('returns tokens array that matches the tokens found in input', () => { - expect(results.tokens[0].key).toBe('author'); - expect(results.tokens[0].value).toBe('@root'); - expect(results.tokens[0].wildcard).toBe(false); - - expect(results.tokens[1].key).toBe('label'); - expect(results.tokens[1].value).toBe('~Very Important'); - expect(results.tokens[1].wildcard).toBe(false); - - expect(results.tokens[2].key).toBe('milestone'); - expect(results.tokens[2].value).toBe('%v1.0'); - expect(results.tokens[2].wildcard).toBe(false); - - expect(results.tokens[3].key).toBe('assignee'); - expect(results.tokens[3].value).toBe('none'); - expect(results.tokens[3].wildcard).toBe(true); - }); - - it('returns lastToken equal to the last object in the tokens array', () => { - expect(results.tokens[3]).toBe(results.lastToken); - }); - }); - - describe('input starts with search value and ends with tokens', () => { - let results; - beforeEach(() => { - results = gl.FilteredSearchTokenizer - .processTokens('searchTerm anotherSearchTerm milestone:none'); - }); - - it('returns searchToken', () => { - expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); - }); - - it('returns correct number of tokens', () => { - expect(results.tokens.length).toBe(1); - }); - - it('returns correct tokens', () => { - expect(results.tokens[0].key).toBe('milestone'); - expect(results.tokens[0].value).toBe('none'); - expect(results.tokens[0].wildcard).toBe(true); - }); - - it('returns lastToken', () => { - expect(results.tokens[0]).toBe(results.lastToken); - }); - }); - - describe('input starts with token and ends with search value', () => { - let results; - beforeEach(() => { - results = gl.FilteredSearchTokenizer - .processTokens('assignee:@user searchTerm'); - }); - - it('returns searchToken', () => { - expect(results.searchToken).toBe('searchTerm'); - }); - - it('returns correct number of tokens', () => { - expect(results.tokens.length).toBe(1); - }); - - it('returns correct tokens', () => { - expect(results.tokens[0].key).toBe('assignee'); - expect(results.tokens[0].value).toBe('@user'); - expect(results.tokens[0].wildcard).toBe(false); - }); - - it('returns lastToken as the searchTerm', () => { - expect(results.lastToken).toBe(results.searchToken); - }); - }); - - describe('input contains search value wrapped between tokens', () => { - let results; - beforeEach(() => { - results = gl.FilteredSearchTokenizer - .processTokens('author:@root label:~"Won\'t fix" searchTerm anotherSearchTerm milestone:none'); - }); - - it('returns searchToken', () => { - expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); - }); - - it('returns correct number of tokens', () => { - expect(results.tokens.length).toBe(3); - }); - - - it('returns tokens array in the order it was processed', () => { - expect(results.tokens[0].key).toBe('author'); - expect(results.tokens[0].value).toBe('@root'); - expect(results.tokens[0].wildcard).toBe(false); - - expect(results.tokens[1].key).toBe('label'); - expect(results.tokens[1].value).toBe('~Won\'t fix'); - expect(results.tokens[1].wildcard).toBe(false); - - expect(results.tokens[2].key).toBe('milestone'); - expect(results.tokens[2].value).toBe('none'); - expect(results.tokens[2].wildcard).toBe(true); - }); - - it('returns lastToken', () => { - expect(results.tokens[2]).toBe(results.lastToken); - }); - }); - - describe('input search value is spaced in between tokens', () => { - let results; - beforeEach(() => { - results = gl.FilteredSearchTokenizer - .processTokens('author:@root searchTerm assignee:none anotherSearchTerm label:~Doing'); - }); - - it('returns searchToken', () => { - expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); - }); - - it('returns correct number of tokens', () => { - expect(results.tokens.length).toBe(3); - }); - - it('returns tokens array in the order it was processed', () => { - expect(results.tokens[0].key).toBe('author'); - expect(results.tokens[0].value).toBe('@root'); - expect(results.tokens[0].wildcard).toBe(false); - - expect(results.tokens[1].key).toBe('assignee'); - expect(results.tokens[1].value).toBe('none'); - expect(results.tokens[1].wildcard).toBe(true); - - expect(results.tokens[2].key).toBe('label'); - expect(results.tokens[2].value).toBe('~Doing'); - expect(results.tokens[2].wildcard).toBe(false); - }); - - it('returns lastToken', () => { - expect(results.tokens[2]).toBe(results.lastToken); - }); + it('returns for input containing only search value', () => { + const results = gl.FilteredSearchTokenizer.processTokens('searchTerm'); + expect(results.searchToken).toBe('searchTerm'); + expect(results.tokens.length).toBe(0); + expect(results.lastToken).toBe(results.searchToken); + }); + + it('returns for input containing only tokens', () => { + const results = gl.FilteredSearchTokenizer + .processTokens('author:@root label:~"Very Important" milestone:%v1.0 assignee:none'); + expect(results.searchToken).toBe(''); + expect(results.tokens.length).toBe(4); + expect(results.tokens[3]).toBe(results.lastToken); + + expect(results.tokens[0].key).toBe('author'); + expect(results.tokens[0].value).toBe('root'); + expect(results.tokens[0].symbol).toBe('@'); + + expect(results.tokens[1].key).toBe('label'); + expect(results.tokens[1].value).toBe('Very Important'); + expect(results.tokens[1].symbol).toBe('~'); + + expect(results.tokens[2].key).toBe('milestone'); + expect(results.tokens[2].value).toBe('v1.0'); + expect(results.tokens[2].symbol).toBe('%'); + + expect(results.tokens[3].key).toBe('assignee'); + expect(results.tokens[3].value).toBe('none'); + expect(results.tokens[3].symbol).toBe(''); + }); + + it('returns for input starting with search value and ending with tokens', () => { + const results = gl.FilteredSearchTokenizer + .processTokens('searchTerm anotherSearchTerm milestone:none'); + expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); + expect(results.tokens.length).toBe(1); + expect(results.tokens[0]).toBe(results.lastToken); + expect(results.tokens[0].key).toBe('milestone'); + expect(results.tokens[0].value).toBe('none'); + expect(results.tokens[0].symbol).toBe(''); + }); + + it('returns for input starting with tokens and ending with search value', () => { + const results = gl.FilteredSearchTokenizer + .processTokens('assignee:@user searchTerm'); + + expect(results.searchToken).toBe('searchTerm'); + expect(results.tokens.length).toBe(1); + expect(results.tokens[0].key).toBe('assignee'); + expect(results.tokens[0].value).toBe('user'); + expect(results.tokens[0].symbol).toBe('@'); + expect(results.lastToken).toBe(results.searchToken); + }); + + it('returns for input containing search value wrapped between tokens', () => { + const results = gl.FilteredSearchTokenizer + .processTokens('author:@root label:~"Won\'t fix" searchTerm anotherSearchTerm milestone:none'); + + expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); + expect(results.tokens.length).toBe(3); + expect(results.tokens[2]).toBe(results.lastToken); + + expect(results.tokens[0].key).toBe('author'); + expect(results.tokens[0].value).toBe('root'); + expect(results.tokens[0].symbol).toBe('@'); + + expect(results.tokens[1].key).toBe('label'); + expect(results.tokens[1].value).toBe('Won\'t fix'); + expect(results.tokens[1].symbol).toBe('~'); + + expect(results.tokens[2].key).toBe('milestone'); + expect(results.tokens[2].value).toBe('none'); + expect(results.tokens[2].symbol).toBe(''); + }); + + it('returns for input containing search value in between tokens', () => { + const results = gl.FilteredSearchTokenizer + .processTokens('author:@root searchTerm assignee:none anotherSearchTerm label:~Doing'); + expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); + expect(results.tokens.length).toBe(3); + expect(results.tokens[2]).toBe(results.lastToken); + + expect(results.tokens[0].key).toBe('author'); + expect(results.tokens[0].value).toBe('root'); + expect(results.tokens[0].symbol).toBe('@'); + + expect(results.tokens[1].key).toBe('assignee'); + expect(results.tokens[1].value).toBe('none'); + expect(results.tokens[1].symbol).toBe(''); + + expect(results.tokens[2].key).toBe('label'); + expect(results.tokens[2].value).toBe('Doing'); + expect(results.tokens[2].symbol).toBe('~'); }); }); }); -- cgit v1.2.1 From f72c1bf1c930b4dcb533202204d132f42246d99f Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 17 Dec 2016 19:55:44 -0600 Subject: Fix specs --- .../javascripts/filtered_search/dropdown_utils.js.es6 | 13 +++++++++---- .../filtered_search_dropdown_manager.js.es6 | 15 +++++++++++---- .../issues/filtered_search/dropdown_assignee_spec.rb | 6 +++--- .../issues/filtered_search/dropdown_author_spec.rb | 8 +++++--- .../issues/filtered_search/dropdown_label_spec.rb | 2 +- .../features/issues/filtered_search/filter_issues_spec.rb | 2 +- 6 files changed, 30 insertions(+), 16 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 index d246000ff52..0c0d24d4de8 100644 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 @@ -42,12 +42,17 @@ static filterHint(item, query) { const updatedItem = item; - const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); + let { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); + lastToken = lastToken || ''; - if (!lastToken) { + if (!lastToken || query.split('').last() === ' ') { updatedItem.droplab_hidden = false; - } else { - updatedItem.droplab_hidden = updatedItem.hint.indexOf(lastToken.toLowerCase()) === -1; + } else if (lastToken) { + const split = lastToken.split(':'); + const tokenName = split[0].split(' ').last(); + + const match = updatedItem.hint.indexOf(tokenName.toLowerCase()) === -1; + updatedItem.droplab_hidden = tokenName ? match : false; } return updatedItem; diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 index b67176267fb..e9f1fbf63ed 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 @@ -62,6 +62,7 @@ const value = input.value; const hasExistingValue = value.length !== 0; const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(value); + const lastSearchToken = searchToken.split(' ').last(); // Find out what part of the token value the user has typed // and remove it from input before appending the selected token value @@ -74,8 +75,8 @@ // Add 2 length to account for the length of the front and back quotes const lengthToRemove = hasQuotes ? lastTokenString.length + 2 : lastTokenString.length; input.value = value.slice(0, -1 * (lengthToRemove)); - } else if (searchToken !== '' && word.indexOf(searchToken) !== -1) { - input.value = value.slice(0, -1 * searchToken.length); + } else if (searchToken !== '' && word.indexOf(lastSearchToken) !== -1) { + input.value = value.slice(0, -1 * lastSearchToken.length); } input.value += hasExistingValue && addSpace ? ` ${word}` : word; @@ -150,11 +151,17 @@ const { lastToken, searchToken } = this.tokenizer .processTokens(this.filteredSearchInput.value); - if (lastToken === searchToken) { + if (this.filteredSearchInput.value.split('').last() === ' ') { + this.updateCurrentDropdownOffset(); + } + + if (lastToken === searchToken && lastToken !== null) { // Token is not fully initialized yet because it has no value // Eg. token = 'label:' + const split = lastToken.split(':'); - this.loadDropdown(split.length > 1 ? split[0] : ''); + const dropdownName = split[0].split(' ').last(); + this.loadDropdown(split.length > 1 ? dropdownName : ''); } else if (lastToken) { // Token has been initialized into an object because it has a value this.loadDropdown(lastToken.key); diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb index 5d0e95580c9..a5a05a622b6 100644 --- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb @@ -13,7 +13,7 @@ describe 'Dropdown assignee', js: true, feature: true do def send_keys_to_filtered_search(input) input.split("").each do |i| filtered_search.send_keys(i) - sleep 3 + sleep 5 wait_for_ajax end end @@ -65,7 +65,7 @@ describe 'Dropdown assignee', js: true, feature: true do describe 'filtering' do before do - filtered_search.set('assignee:') + send_keys_to_filtered_search('assignee:') end it 'filters by name' do @@ -118,7 +118,7 @@ describe 'Dropdown assignee', js: true, feature: true do end it 'selects `no assignee`' do - click_assignee('No Assignee') + find('#js-dropdown-assignee .filter-dropdown-item', text: 'No Assignee').click expect(page).to have_css(js_dropdown_assignee, visible: false) expect(filtered_search.value).to eq("assignee:none") end diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb index c7cb5e25174..956f7156c5f 100644 --- a/spec/features/issues/filtered_search/dropdown_author_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb @@ -13,7 +13,7 @@ describe 'Dropdown author', js: true, feature: true do def send_keys_to_filtered_search(input) input.split("").each do |i| filtered_search.send_keys(i) - sleep 3 + sleep 5 wait_for_ajax end end @@ -65,7 +65,8 @@ describe 'Dropdown author', js: true, feature: true do describe 'filtering' do before do - filtered_search.set('author:') + filtered_search.set('author') + send_keys_to_filtered_search(':') end it 'filters by name' do @@ -101,7 +102,8 @@ describe 'Dropdown author', js: true, feature: true do describe 'selecting from dropdown' do before do - filtered_search.set('author:') + filtered_search.set('author') + send_keys_to_filtered_search(':') end it 'fills in the author username when the author has not been filtered' do diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb index d007e160b3e..a0e2973d6b8 100644 --- a/spec/features/issues/filtered_search/dropdown_label_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb @@ -176,7 +176,7 @@ describe 'Dropdown label', js: true, feature: true do end it 'selects `no label`' do - click_label('No Label') + find('#js-dropdown-label .filter-dropdown-item', text: 'No Label').click expect(page).to have_css(js_dropdown_label, visible: false) expect(filtered_search.value).to eq("label:none") end diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index e72417aea80..ed6d4a0787a 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -17,9 +17,9 @@ describe 'Filter issues', js: true, feature: true do let!(:multiple_words_label) { create(:label, project: project, title: "Two words") } let!(:closed_issue) { create(:issue, title: 'bug that is closed', project: project, state: :closed) } + let(:filtered_search) { find('.filtered-search') } def input_filtered_search(search_term) - filtered_search = find('.filtered-search') filtered_search.set(search_term) filtered_search.send_keys(:enter) end -- cgit v1.2.1 From 12753def903f265467d2cab8e19deced31daf066 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sun, 18 Dec 2016 17:04:29 -0600 Subject: Fix specs --- app/assets/javascripts/dispatcher.js.es6 | 2 +- .../filtered_search/dropdown_utils.js.es6 | 2 +- .../issues/filtered_search/dropdown_author_spec.rb | 13 +++---- .../issues/filtered_search/dropdown_label_spec.rb | 40 +++++++++++++--------- .../filtered_search/dropdown_milestone_spec.rb | 39 ++++----------------- .../filtered_search_dropdown_manager_spec.js.es6 | 2 -- 6 files changed, 36 insertions(+), 62 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 1e9111f4718..ca2da18dc26 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -84,7 +84,7 @@ break; case 'projects:merge_requests:index': case 'projects:issues:index': - if(document.querySelector('.filtered-search') && gl.FilteredSearchManager) { + if (document.querySelector('.filtered-search') && gl.FilteredSearchManager) { new gl.FilteredSearchManager(); } Issuable.init(); diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 index 0c0d24d4de8..3453311bee5 100644 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 @@ -43,7 +43,7 @@ static filterHint(item, query) { const updatedItem = item; let { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); - lastToken = lastToken || ''; + lastToken = lastToken.key || lastToken || ''; if (!lastToken || query.split('').last() === ' ') { updatedItem.droplab_hidden = false; diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb index 956f7156c5f..d053b032768 100644 --- a/spec/features/issues/filtered_search/dropdown_author_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb @@ -70,13 +70,13 @@ describe 'Dropdown author', js: true, feature: true do end it 'filters by name' do - send_keys_to_filtered_search('j') - expect(dropdown_author_size).to eq(2) + send_keys_to_filtered_search('ja') + expect(dropdown_author_size).to eq(1) end it 'filters by case insensitive name' do - send_keys_to_filtered_search('J') - expect(dropdown_author_size).to eq(2) + send_keys_to_filtered_search('Ja') + expect(dropdown_author_size).to eq(1) end it 'filters by username with symbol' do @@ -84,11 +84,6 @@ describe 'Dropdown author', js: true, feature: true do expect(dropdown_author_size).to eq(2) end - it 'filters by case insensitive username with symbol' do - send_keys_to_filtered_search('@OT') - expect(dropdown_author_size).to eq(2) - end - it 'filters by username without symbol' do send_keys_to_filtered_search('ot') expect(dropdown_author_size).to eq(2) diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb index a0e2973d6b8..1a21b0d64ed 100644 --- a/spec/features/issues/filtered_search/dropdown_label_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb @@ -9,6 +9,7 @@ describe 'Dropdown label', js: true, feature: true do let!(:uppercase_label) { create(:label, project: project, title: 'BUG') } let!(:two_words_label) { create(:label, project: project, title: 'High Priority') } let!(:wont_fix_label) { create(:label, project: project, title: 'Won"t Fix') } + let!(:wont_fix_single_label) { create(:label, project: project, title: 'Won\'t Fix') } let!(:special_label) { create(:label, project: project, title: '!@#$%^+&*()')} let!(:long_label) { create(:label, project: project, title: 'this is a very long title this is a very long title this is a very long title this is a very long title this is a very long title')} let(:filtered_search) { find('.filtered-search') } @@ -68,61 +69,66 @@ describe 'Dropdown label', js: true, feature: true do describe 'filtering' do before do - filtered_search.set('label:') + filtered_search.set('label') end it 'filters by name' do - send_keys_to_filtered_search('b') + send_keys_to_filtered_search(':b') expect(dropdown_label_size).to eq(2) end it 'filters by case insensitive name' do - send_keys_to_filtered_search('B') + send_keys_to_filtered_search(':B') expect(dropdown_label_size).to eq(2) end it 'filters by name with symbol' do - send_keys_to_filtered_search('~bu') + send_keys_to_filtered_search(':~bu') expect(dropdown_label_size).to eq(2) end it 'filters by case insensitive name with symbol' do - send_keys_to_filtered_search('~BU') + send_keys_to_filtered_search(':~BU') expect(dropdown_label_size).to eq(2) end - it 'filters by multiple names using double quotes' do - send_keys_to_filtered_search('"High P') + it 'filters by multiple words' do + send_keys_to_filtered_search(':Hig') + expect(dropdown_label_size).to eq(1) + end + + it 'filters by multiple words with symbol' do + send_keys_to_filtered_search(':~Hig') expect(dropdown_label_size).to eq(1) end - it 'filters by multiple names using single quotes' do - send_keys_to_filtered_search('\'High P') + it 'filters by multiple words containing single quotes' do + send_keys_to_filtered_search(':won\'t') expect(dropdown_label_size).to eq(1) end - it 'filters by multiple names using single and double quotes' do - send_keys_to_filtered_search('~"won`\'t f') + it 'filters by multiple words containing single quotes with symbol' do + send_keys_to_filtered_search(':~won\'t') expect(dropdown_label_size).to eq(1) end - it 'filters by multiple names using double quotes with symbol' do - send_keys_to_filtered_search('~"High P') + it 'filters by multiple words containing double quotes' do + send_keys_to_filtered_search(':won"t') expect(dropdown_label_size).to eq(1) end - it 'filters by multiple names using single quotes with symbol' do - send_keys_to_filtered_search('~\'High P') + it 'filters by multiple words containing double quotes with symbol' do + send_keys_to_filtered_search(':~won"t') expect(dropdown_label_size).to eq(1) end it 'filters by special characters' do - send_keys_to_filtered_search('^+') + send_keys_to_filtered_search(':^+') expect(dropdown_label_size).to eq(1) end it 'filters by special characters with symbol' do - send_keys_to_filtered_search('~^+') + send_keys_to_filtered_search(':~^+') expect(dropdown_label_size).to eq(1) end end diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb index d367430a670..64fc83c6ccb 100644 --- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb @@ -73,61 +73,36 @@ describe 'Dropdown milestone', js: true, feature: true do describe 'filtering' do before do - filtered_search.set('milestone:') + filtered_search.set('milestone') end it 'filters by name' do - send_keys_to_filtered_search('v1') + send_keys_to_filtered_search(':v1') expect(dropdown_milestone_size).to eq(1) end it 'filters by case insensitive name' do - send_keys_to_filtered_search('V1') + send_keys_to_filtered_search(':V1') expect(dropdown_milestone_size).to eq(1) end it 'filters by name with symbol' do - send_keys_to_filtered_search('%v1') + send_keys_to_filtered_search(':%v1') expect(dropdown_milestone_size).to eq(1) end it 'filters by case insensitive name with symbol' do - send_keys_to_filtered_search('%V1') - expect(dropdown_milestone_size).to eq(1) - end - - it 'filters by multiple names using double quotes' do - send_keys_to_filtered_search('"future') - expect(dropdown_milestone_size).to eq(1) - end - - it 'filters by multiple names using single quotes' do - send_keys_to_filtered_search('\'future p') - expect(dropdown_milestone_size).to eq(1) - end - - it 'filters by multiple names using single and double quotes' do - send_keys_to_filtered_search('%"won`\'t f') - expect(dropdown_milestone_size).to eq(1) - end - - it 'filters by multiple names using double quotes with symbol' do - send_keys_to_filtered_search('%"future p') - expect(dropdown_milestone_size).to eq(1) - end - - it 'filters by multiple names using single quotes with symbol' do - send_keys_to_filtered_search('%\'future p') + send_keys_to_filtered_search(':%V1') expect(dropdown_milestone_size).to eq(1) end it 'filters by special characters' do - send_keys_to_filtered_search('^+') + send_keys_to_filtered_search(':(+') expect(dropdown_milestone_size).to eq(1) end it 'filters by special characters with symbol' do - send_keys_to_filtered_search('~^+') + send_keys_to_filtered_search(':%(+') expect(dropdown_milestone_size).to eq(1) end end diff --git a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 index 562673a4ee5..e9841e3c89e 100644 --- a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 +++ b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 @@ -12,8 +12,6 @@ const input = document.createElement('input'); input.classList.add('filtered-search'); document.body.appendChild(input); - - expect(input.value).toBe(''); }); afterEach(() => { -- cgit v1.2.1 From b0d8d742c588baacf0ba08074f23e68498297d10 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 16 Dec 2016 19:52:54 +0800 Subject: Pass the arguments from where we render the partial Thread: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7345/diffs#note_19707619 --- app/views/projects/issues/index.html.haml | 2 +- app/views/shared/issuable/_search_bar.html.haml | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 18e8372ecab..6585e8de1e7 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -32,7 +32,7 @@ title: "New Issue", id: "new_issue_link" do New Issue - = render 'shared/issuable/search_bar', type: :issues + = render 'shared/issuable/search_bar', type: :issues, finder: issues_finder .issues-holder = render 'issues' diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 3449c1f0151..b65d523ddc8 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -1,3 +1,6 @@ +- type = local_assigns.fetch(:type) +- finder = local_assigns.fetch(:finder) + .issues-filters .issues-details-filters.row-content-block.second-block.filtered-search-block = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do -- cgit v1.2.1 From 866bb202f29b8f5ad52563dd48ae57168dc6df77 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 16 Dec 2016 19:58:59 +0800 Subject: Check if it's not NONE too So that we don't have to check it again in somewhere else, and we don't really need to know if it's presented as NONE Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7345/diffs#note_20123999 --- app/finders/issuable_finder.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 2afde8ece65..dfd7de6afa9 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -166,20 +166,20 @@ class IssuableFinder end def assignee_id? - params[:assignee_id].present? + params[:assignee_id].present? && params[:assignee_id] != NONE end def assignee_username? - params[:assignee_username].present? + params[:assignee_username].present? && params[:assignee_username] != NONE end def assignee return @assignee if defined?(@assignee) @assignee = - if assignee_id? && params[:assignee_id] != NONE + if assignee_id? User.find(params[:assignee_id]) - elsif assignee_username? && params[:assignee_username] != NONE + elsif assignee_username? User.find_by(username: params[:assignee_username]) else nil @@ -187,11 +187,11 @@ class IssuableFinder end def author_id? - params[:author_id].present? + params[:author_id].present? && params[:author_id] != NONE end def author_username? - params[:author_username].present? + params[:author_username].present? && params[:author_username] != NONE end def author -- cgit v1.2.1 From 464dddf4d0b54085c10ecb8b62aa7816ed7ba8a3 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 16 Dec 2016 20:12:59 +0800 Subject: Show no issues if author/assignee cannot be found Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7345/diffs#note_19994225 --- app/finders/issuable_finder.rb | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index dfd7de6afa9..dce756544e7 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -178,7 +178,7 @@ class IssuableFinder @assignee = if assignee_id? - User.find(params[:assignee_id]) + User.find_by(id: params[:assignee_id]) elsif assignee_username? User.find_by(username: params[:assignee_username]) else @@ -198,9 +198,9 @@ class IssuableFinder return @author if defined?(@author) @author = - if author_id? && params[:author_id] != NONE - User.find(params[:author_id]) - elsif author_username? && params[:author_username] != NONE + if author_id? + User.find_by(id: params[:author_id]) + elsif author_username? User.find_by(username: params[:author_username]) else nil @@ -275,16 +275,20 @@ class IssuableFinder end def by_assignee(items) - if assignee_id? || assignee_username? - items = items.where(assignee_id: assignee.try(:id)) + if assignee + items = items.where(assignee_id: assignee.id) + elsif assignee_id? || assignee_username? # assignee not found + items = items.none end items end def by_author(items) - if author_id? || author_username? - items = items.where(author_id: author.try(:id)) + if author + items = items.where(author_id: author.id) + elsif author_id? || author_username? # author not found + items = items.none end items -- cgit v1.2.1 From 23d43243a1261be0af6142f7d34063587b11d51b Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 16 Dec 2016 20:34:21 +0800 Subject: Fix various styles for rubocop --- spec/features/issues/filtered_search/dropdown_hint_spec.rb | 4 ++-- spec/features/issues/filtered_search/search_bar_spec.rb | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb index c4328bc18b0..910abb56569 100644 --- a/spec/features/issues/filtered_search/dropdown_hint_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb @@ -27,7 +27,7 @@ describe 'Dropdown hint', js: true, feature: true do describe 'behavior' do before do expect(page).to have_css(js_dropdown_hint, visible: false) - filtered_search.click() + filtered_search.click end it 'opens when the search bar is first focused' do @@ -35,7 +35,7 @@ describe 'Dropdown hint', js: true, feature: true do end it 'closes when the search bar is unfocused' do - find('body').click() + find('body').click expect(page).to have_css(js_dropdown_hint, visible: false) end end diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb index 3af36ae27dd..d5db90ae506 100644 --- a/spec/features/issues/filtered_search/search_bar_spec.rb +++ b/spec/features/issues/filtered_search/search_bar_spec.rb @@ -53,14 +53,14 @@ describe 'Search bar', js: true, feature: true do end it 'resets the dropdown hint filter' do - filtered_search.click() + filtered_search.click original_size = page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size filtered_search.set('author') expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(1) find('.filtered-search-input-container .clear-search').click - filtered_search.click() + filtered_search.click expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(original_size) end @@ -73,9 +73,9 @@ describe 'Search bar', js: true, feature: true do expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(0) find('.filtered-search-input-container .clear-search').click - filtered_search.click() + filtered_search.click expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to be > 0 - expect(get_left_style(page.find('#js-dropdown-hint')['style'])).to eq hint_offset + expect(get_left_style(page.find('#js-dropdown-hint')['style'])).to eq(hint_offset) end end end -- cgit v1.2.1 From f7f9e58092892e5bae0887aa2c0ee9f699085aad Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 16 Dec 2016 22:28:18 +0800 Subject: Make sure we could query against no one We should separate the idea of not finding anyone, and the idea of against no one. --- app/finders/issuable_finder.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index dce756544e7..5ffaf5ae0f8 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -173,6 +173,10 @@ class IssuableFinder params[:assignee_username].present? && params[:assignee_username] != NONE end + def no_assignee? + params[:assignee_id] == NONE || params[:assignee_username] == NONE + end + def assignee return @assignee if defined?(@assignee) @@ -194,6 +198,10 @@ class IssuableFinder params[:author_username].present? && params[:author_username] != NONE end + def no_author? + params[:author_id] == NONE || params[:author_username] == NONE + end + def author return @author if defined?(@author) @@ -277,6 +285,8 @@ class IssuableFinder def by_assignee(items) if assignee items = items.where(assignee_id: assignee.id) + elsif no_assignee? + items = items.where(assignee_id: nil) elsif assignee_id? || assignee_username? # assignee not found items = items.none end @@ -287,6 +297,8 @@ class IssuableFinder def by_author(items) if author items = items.where(author_id: author.id) + elsif no_author? + items = items.where(author_id: nil) elsif author_id? || author_username? # author not found items = items.none end -- cgit v1.2.1 From 0a1d8875b715431e7631051e29b47c78b6e135ce Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 19 Dec 2016 06:32:42 +0000 Subject: we're actually not using issue finder here --- app/views/projects/issues/index.html.haml | 2 +- app/views/shared/issuable/_search_bar.html.haml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 6585e8de1e7..18e8372ecab 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -32,7 +32,7 @@ title: "New Issue", id: "new_issue_link" do New Issue - = render 'shared/issuable/search_bar', type: :issues, finder: issues_finder + = render 'shared/issuable/search_bar', type: :issues .issues-holder = render 'issues' diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index b65d523ddc8..8620deb4dfd 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -1,5 +1,4 @@ - type = local_assigns.fetch(:type) -- finder = local_assigns.fetch(:finder) .issues-filters .issues-details-filters.row-content-block.second-block.filtered-search-block -- cgit v1.2.1 From 896497aba37f9e83585443a108e8611e5bc58488 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 5 Jan 2017 10:57:43 -0600 Subject: Fix haml lint --- app/views/shared/issuable/_search_bar.html.haml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 8620deb4dfd..33f96a86723 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -25,7 +25,7 @@ %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item %button.btn.btn-link - %i.fa{ class: '{{icon}}'} + %i.fa{ class: "#{'{{icon}}'}" } %span.js-filter-hint {{hint}} %span.js-filter-tag.dropdown-light-content @@ -34,7 +34,7 @@ %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item %button.btn.btn-link.dropdown-user - %img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', width: '30' } + %img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', alt: '{{name}}\'s avatar', width: '30' } .dropdown-user-details %span {{name}} @@ -49,7 +49,7 @@ %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item %button.btn.btn-link.dropdown-user - %img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', width: '30' } + %img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', alt: '{{name}}\'s avatar', width: '30' } .dropdown-user-details %span {{name}} @@ -77,7 +77,7 @@ %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item %button.btn.btn-link - %span.dropdown-label-box{ style: 'background: {{color}}'} + %span.dropdown-label-box{ style: 'background: {{color}}' } %span.label-title.js-data-value {{title}} .pull-right @@ -90,9 +90,9 @@ = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do %ul %li - %a{href: "#", data: {id: "reopen"}} Open + %a{ href: "#", data: { id: "reopen" } } Open %li - %a{href: "#", data: {id: "close"}} Closed + %a{ href: "#", data: { id: "close" } } Closed .filter-item.inline = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } }) @@ -104,9 +104,9 @@ = dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do %ul %li - %a{href: "#", data: {id: "subscribe"}} Subscribe + %a{ href: "#", data: { id: "subscribe" } } Subscribe %li - %a{href: "#", data: {id: "unsubscribe"}} Unsubscribe + %a{ href: "#", data: { id: "unsubscribe" } } Unsubscribe = hidden_field_tag 'update[issuable_ids]', [] = hidden_field_tag :state_event, params[:state_event] -- cgit v1.2.1 From 8f77b3177f1c342cc0a05c79e45e88bcda04040a Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 5 Jan 2017 11:01:56 -0600 Subject: Add haml lint comment --- app/views/shared/issuable/_search_bar.html.haml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 33f96a86723..8d7b1d616f4 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -25,6 +25,8 @@ %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item %button.btn.btn-link + -# Encapsulate static class name `{{icon}}` inside #{} to bypass + -# haml lint's ClassAttributeWithStaticValue %i.fa{ class: "#{'{{icon}}'}" } %span.js-filter-hint {{hint}} -- cgit v1.2.1 From c349bb15b628039340054eb132201fdf4a740411 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 5 Jan 2017 13:56:23 -0600 Subject: Refactor addWordToInput --- .../filtered_search/dropdown_hint.js.es6 | 6 +-- .../filtered_search/dropdown_non_user.js.es6 | 4 +- .../filtered_search/dropdown_user.js.es6 | 4 +- .../filtered_search/dropdown_utils.js.es6 | 4 +- .../filtered_search_dropdown.js.es6 | 7 +-- .../filtered_search_dropdown_manager.js.es6 | 41 ++++++++-------- .../filtered_search_dropdown_manager_spec.js.es6 | 57 +++++++++------------- 7 files changed, 57 insertions(+), 66 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index c5ab9c52d76..f1e317d91cc 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -4,8 +4,8 @@ (() => { class DropdownHint extends gl.FilteredSearchDropdown { - constructor(droplab, dropdown, input) { - super(droplab, dropdown, input); + constructor(droplab, dropdown, input, filter) { + super(droplab, dropdown, input, filter); this.config = { droplabFilter: { template: 'hint', @@ -25,7 +25,7 @@ const tag = selected.querySelector('.js-filter-tag').innerText.trim(); if (tag.length) { - gl.FilteredSearchDropdownManager.addWordToInput(token); + gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', '')); } this.dismissDropdown(); this.dispatchInputEvent(); diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 index 54090375c5c..f06c3fc9c6f 100644 --- a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 @@ -5,8 +5,8 @@ (() => { class DropdownNonUser extends gl.FilteredSearchDropdown { - constructor(droplab, dropdown, input, endpoint, symbol) { - super(droplab, dropdown, input); + constructor(droplab, dropdown, input, filter, endpoint, symbol) { + super(droplab, dropdown, input, filter); this.symbol = symbol; this.config = { droplabAjax: { diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 index 49581e3bfbd..e80d266ae89 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 @@ -4,8 +4,8 @@ (() => { class DropdownUser extends gl.FilteredSearchDropdown { - constructor(droplab, dropdown, input) { - super(droplab, dropdown, input); + constructor(droplab, dropdown, input, filter) { + super(droplab, dropdown, input, filter); this.config = { droplabAjaxFilter: { endpoint: '/autocomplete/users.json', diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 index 3453311bee5..88b172d6fc4 100644 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 @@ -58,11 +58,11 @@ return updatedItem; } - static setDataValueIfSelected(selected) { + static setDataValueIfSelected(filter, selected) { const dataValue = selected.getAttribute('data-value'); if (dataValue) { - gl.FilteredSearchDropdownManager.addWordToInput(dataValue); + gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue); } // Return boolean based on whether it was set diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 index 56147ad93c9..886d8113f4a 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -2,10 +2,11 @@ const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger'; class FilteredSearchDropdown { - constructor(droplab, dropdown, input) { + constructor(droplab, dropdown, input, filter) { this.droplab = droplab; this.hookId = input.getAttribute('data-id'); this.input = input; + this.filter = filter; this.dropdown = dropdown; this.loadingTemplate = `
@@ -30,11 +31,11 @@ const { selected } = e.detail; if (selected.tagName === 'LI' && selected.innerHTML) { - const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(selected); + const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(this.filter, selected); if (!dataValueSet) { const value = getValueFunction(selected); - gl.FilteredSearchDropdownManager.addWordToInput(value); + gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value); } this.dismissDropdown(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 index e9f1fbf63ed..8b385d6b642 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 @@ -55,31 +55,30 @@ }; } - static addWordToInput(word, addSpace = false) { + static addWordToInput(tokenName, tokenValue = '') { const input = document.querySelector('.filtered-search'); - input.value = input.value.trim(); + const word = `${tokenName}:${tokenValue}`; - const value = input.value; - const hasExistingValue = value.length !== 0; - const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(value); + const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(input.value); const lastSearchToken = searchToken.split(' ').last(); - - // Find out what part of the token value the user has typed - // and remove it from input before appending the selected token value - if (lastToken !== searchToken) { - const lastTokenString = `${lastToken.symbol}${lastToken.value}`; - - // Spaces inside the token means that the token value will be escaped by quotes - const hasQuotes = lastTokenString.indexOf(' ') !== -1; - - // Add 2 length to account for the length of the front and back quotes - const lengthToRemove = hasQuotes ? lastTokenString.length + 2 : lastTokenString.length; - input.value = value.slice(0, -1 * (lengthToRemove)); - } else if (searchToken !== '' && word.indexOf(lastSearchToken) !== -1) { - input.value = value.slice(0, -1 * lastSearchToken.length); + const lastInputCharacter = input.value[input.value.length - 1]; + const lastInputTrimmedCharacter = input.value.trim()[input.value.trim().length - 1]; + + // Remove the typed tokenName + if (word.indexOf(lastSearchToken) === 0 && searchToken !== '') { + // Remove spaces after the colon + if (lastInputCharacter === ' ' && lastInputTrimmedCharacter === ':') { + input.value = input.value.trim(); + } + + input.value = input.value.slice(0, -1 * lastSearchToken.length); + } else if (lastInputCharacter !== ' ') { + // Remove the existing tokenValue + const lastTokenString = `${lastToken.key}:${lastToken.symbol}${lastToken.value}`; + input.value = input.value.slice(0, -1 * lastTokenString.length); } - input.value += hasExistingValue && addSpace ? ` ${word}` : word; + input.value += word; } updateCurrentDropdownOffset() { @@ -106,7 +105,7 @@ if (!mappingKey.reference) { const dl = this.droplab; - const defaultArguments = [null, dl, element, this.filteredSearchInput]; + const defaultArguments = [null, dl, element, this.filteredSearchInput, key]; const glArguments = defaultArguments.concat(mappingKey.extraArguments || []); // Passing glArguments to `new gl[glClass]()` diff --git a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 index e9841e3c89e..d0d27ceb4a6 100644 --- a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 +++ b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 @@ -1,3 +1,4 @@ +//= require extensions/array //= require filtered_search/filtered_search_tokenizer //= require filtered_search/filtered_search_dropdown_manager @@ -8,6 +9,10 @@ return document.querySelector('.filtered-search').value; } + function setInputValue(value) { + document.querySelector('.filtered-search').value = value; + } + beforeEach(() => { const input = document.createElement('input'); input.classList.add('filtered-search'); @@ -19,47 +24,33 @@ }); describe('input has no existing value', () => { - it('should add word', () => { - gl.FilteredSearchDropdownManager.addWordToInput('firstWord'); - expect(getInputValue()).toBe('firstWord'); - }); - - it('should not add space before first word', () => { - gl.FilteredSearchDropdownManager.addWordToInput('firstWord', true); - expect(getInputValue()).toBe('firstWord'); + it('should add just tokenName', () => { + gl.FilteredSearchDropdownManager.addWordToInput('milestone'); + expect(getInputValue()).toBe('milestone:'); }); - it('should not add space before second word by default', () => { - gl.FilteredSearchDropdownManager.addWordToInput('firstWord'); - expect(getInputValue()).toBe('firstWord'); - gl.FilteredSearchDropdownManager.addWordToInput('secondWord'); - expect(getInputValue()).toBe('firstWordsecondWord'); - }); - - it('should add space before new word when addSpace is passed', () => { - expect(getInputValue()).toBe(''); - gl.FilteredSearchDropdownManager.addWordToInput('firstWord'); - expect(getInputValue()).toBe('firstWord'); - gl.FilteredSearchDropdownManager.addWordToInput('secondWord', true); - expect(getInputValue()).toBe('firstWord secondWord'); + it('should add tokenName and tokenValue', () => { + gl.FilteredSearchDropdownManager.addWordToInput('label', 'none'); + expect(getInputValue()).toBe('label:none'); }); }); - describe('input has exsting value', () => { - it('should only add the remaining characters of the word', () => { - const lastToken = { - key: 'author', - value: 'roo', - }; + describe('input has existing value', () => { + it('should be able to just add tokenName', () => { + setInputValue('a'); + gl.FilteredSearchDropdownManager.addWordToInput('author'); + expect(getInputValue()).toBe('author:'); + }); - document.querySelector('.filtered-search').value = `${lastToken.key}:${lastToken.value}`; - gl.FilteredSearchDropdownManager.addWordToInput('root'); - expect(getInputValue()).toBe('author:root'); + it('should replace tokenValue', () => { + setInputValue('author:roo'); + gl.FilteredSearchDropdownManager.addWordToInput('author', '@root'); + expect(getInputValue()).toBe('author:@root'); }); - it('should only add the remaining characters of the word (contains space)', () => { - document.querySelector('.filtered-search').value = 'label:~"test'; - gl.FilteredSearchDropdownManager.addWordToInput('~\'"test me"\''); + it('should add tokenValues containing spaces', () => { + setInputValue('label:~"test'); + gl.FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\''); expect(getInputValue()).toBe('label:~\'"test me"\''); }); }); -- cgit v1.2.1 From d5dee97becf193627f407815aa4013ea3c0a47a2 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 6 Jan 2017 12:47:00 -0600 Subject: Enable filtering with multiple words --- app/assets/javascripts/filtered_search/dropdown_utils.js.es6 | 6 +++++- .../filtered_search/filtered_search_dropdown_manager.js.es6 | 2 +- .../javascripts/filtered_search/filtered_search_tokenizer.js.es6 | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 index 88b172d6fc4..c27ef3042d1 100644 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 @@ -25,8 +25,12 @@ const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(query); if (lastToken !== searchToken) { - const value = lastToken.value.toLowerCase(); const title = updatedItem.title.toLowerCase(); + let value = lastToken.value.toLowerCase(); + + if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) { + value = value.slice(1); + } // Eg. filterSymbol = ~ for labels const matchWithoutSymbol = lastToken.symbol === filterSymbol && title.indexOf(value) !== -1; diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 index 8b385d6b642..1cd0483877a 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 @@ -72,7 +72,7 @@ } input.value = input.value.slice(0, -1 * lastSearchToken.length); - } else if (lastInputCharacter !== ' ') { + } else if (lastInputCharacter !== ' ' || (lastToken && lastToken.value[lastToken.value.length - 1] === ' ')) { // Remove the existing tokenValue const lastTokenString = `${lastToken.key}:${lastToken.symbol}${lastToken.value}`; input.value = input.value.slice(0, -1 * lastTokenString.length); diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 index 14ca78e139b..60473dddead 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 @@ -1,7 +1,7 @@ (() => { class FilteredSearchTokenizer { static processTokens(input) { - const tokenRegex = /(\w+):([~%@]?)(?:"(.*?)"|'(.*?)'|(\S+))/g; + const tokenRegex = /(\w+):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\S+))/g; const tokens = []; let lastToken = null; const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => { -- cgit v1.2.1 From 0f973c28b7f4852119181b549255308c76924c4e Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 6 Jan 2017 13:56:34 -0600 Subject: Fix specs --- .../javascripts/filtered_search/filtered_search_manager.js.es6 | 9 ++++++++- spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 | 7 ++++--- .../filtered_search/filtered_search_tokenizer_spec.js.es6 | 4 ++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index d2ea4de18aa..bd3c4240f13 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -142,7 +142,14 @@ if (condition) { tokenPath = condition.url; } else { - tokenPath = `${keyParam}=${encodeURIComponent(token.value)}`; + let tokenValue = token.value; + + if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') || + (tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) { + tokenValue = tokenValue.slice(1, tokenValue.length - 1); + } + + tokenPath = `${keyParam}=${encodeURIComponent(tokenValue)}`; } paths.push(tokenPath); diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 b/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 index 369eb90e31c..ce61b73aa8a 100644 --- a/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 +++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 @@ -1,3 +1,4 @@ +//= require extensions/array //= require filtered_search/dropdown_utils //= require filtered_search/filtered_search_tokenizer //= require filtered_search/filtered_search_dropdown_manager @@ -80,7 +81,7 @@ getAttribute: () => 'value', }; - gl.DropdownUtils.setDataValueIfSelected(selected); + gl.DropdownUtils.setDataValueIfSelected(null, selected); expect(gl.FilteredSearchDropdownManager.addWordToInput.calls.count()).toEqual(1); }); @@ -89,7 +90,7 @@ getAttribute: () => 'value', }; - const result = gl.DropdownUtils.setDataValueIfSelected(selected); + const result = gl.DropdownUtils.setDataValueIfSelected(null, selected); expect(result).toBe(true); }); @@ -98,7 +99,7 @@ getAttribute: () => null, }; - const result = gl.DropdownUtils.setDataValueIfSelected(selected); + const result = gl.DropdownUtils.setDataValueIfSelected(null, selected); expect(result).toBe(false); }); }); diff --git a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 index 8e5992fa446..ac7f8e9cbcd 100644 --- a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 +++ b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 @@ -24,7 +24,7 @@ expect(results.tokens[0].symbol).toBe('@'); expect(results.tokens[1].key).toBe('label'); - expect(results.tokens[1].value).toBe('Very Important'); + expect(results.tokens[1].value).toBe('"Very Important"'); expect(results.tokens[1].symbol).toBe('~'); expect(results.tokens[2].key).toBe('milestone'); @@ -72,7 +72,7 @@ expect(results.tokens[0].symbol).toBe('@'); expect(results.tokens[1].key).toBe('label'); - expect(results.tokens[1].value).toBe('Won\'t fix'); + expect(results.tokens[1].value).toBe('"Won\'t fix"'); expect(results.tokens[1].symbol).toBe('~'); expect(results.tokens[2].key).toBe('milestone'); -- cgit v1.2.1 From 3d0b0a62609d5f961d2777f497558d90c1dc039b Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 6 Jan 2017 14:05:53 -0600 Subject: Add symbols --- app/assets/javascripts/filtered_search/dropdown_hint.js.es6 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index f1e317d91cc..63c20f57520 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -37,19 +37,19 @@ const dropdownData = [{ icon: 'fa-pencil', hint: 'author:', - tag: '<author>', + tag: '<@author>', }, { icon: 'fa-user', hint: 'assignee:', - tag: '<assignee>', + tag: '<@assignee>', }, { icon: 'fa-clock-o', hint: 'milestone:', - tag: '<milestone>', + tag: '<%milestone>', }, { icon: 'fa-tag', hint: 'label:', - tag: '<label>', + tag: '<~label>', }]; this.droplab.changeHookList(this.hookId, this.dropdown, [droplabFilter], this.config); -- cgit v1.2.1 From 1bfdad5c4707c9dac243cebaf4666fee77f17891 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 9 Jan 2017 11:33:17 -0600 Subject: Code review changes --- app/assets/javascripts/dispatcher.js.es6 | 2 +- app/assets/javascripts/droplab/droplab_ajax.js | 2 +- app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index ca2da18dc26..99a34651639 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -84,7 +84,7 @@ break; case 'projects:merge_requests:index': case 'projects:issues:index': - if (document.querySelector('.filtered-search') && gl.FilteredSearchManager) { + if (gl.FilteredSearchManager) { new gl.FilteredSearchManager(); } Issuable.init(); diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js index ebb518eeef4..926e53e696f 100644 --- a/app/assets/javascripts/droplab/droplab_ajax.js +++ b/app/assets/javascripts/droplab/droplab_ajax.js @@ -39,7 +39,7 @@ require('../window')(function(w){ var loadingTemplate = document.createElement('div'); loadingTemplate.innerHTML = config.loadingTemplate; - loadingTemplate.setAttribute('data-loading-template', true); + loadingTemplate.setAttribute('data-loading-template', ''); this.listTemplate = dynamicList.outerHTML; dynamicList.outerHTML = loadingTemplate.outerHTML; diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 index 60473dddead..cf53845a48b 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 @@ -1,6 +1,8 @@ (() => { class FilteredSearchTokenizer { static processTokens(input) { + // Regex extracts `(token):(symbol)(value)` + // Values that start with a double quote must end in a double quote (same for single) const tokenRegex = /(\w+):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\S+))/g; const tokens = []; let lastToken = null; -- cgit v1.2.1 From 4ec8eb9abefb1a2abed9b5cbee4292325b3c22f6 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 9 Jan 2017 12:15:10 -0600 Subject: Fix javascript error for when there are no issues --- .../filtered_search/filtered_search_manager.js.es6 | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index bd3c4240f13..ffd0d7e9cba 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -3,17 +3,20 @@ (() => { class FilteredSearchManager { constructor() { - this.tokenizer = gl.FilteredSearchTokenizer; this.filteredSearchInput = document.querySelector('.filtered-search'); this.clearSearchButton = document.querySelector('.clear-search'); - this.dropdownManager = new gl.FilteredSearchDropdownManager(); - this.bindEvents(); - this.loadSearchParamsFromURL(); - this.dropdownManager.setDropdown(); + if (this.filteredSearchInput) { + this.tokenizer = gl.FilteredSearchTokenizer; + this.dropdownManager = new gl.FilteredSearchDropdownManager(); - this.cleanupWrapper = this.cleanup.bind(this); - document.addEventListener('page:fetch', this.cleanupWrapper); + this.bindEvents(); + this.loadSearchParamsFromURL(); + this.dropdownManager.setDropdown(); + + this.cleanupWrapper = this.cleanup.bind(this); + document.addEventListener('page:fetch', this.cleanupWrapper); + } } cleanup() { -- cgit v1.2.1 From c0287e69c6717fe9de5ff0b804f952410e453ef8 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 9 Jan 2017 15:43:17 -0600 Subject: Fix indentation --- app/assets/javascripts/droplab/droplab_ajax.js | 29 +++++++++++++------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js index 926e53e696f..c8850f121d7 100644 --- a/app/assets/javascripts/droplab/droplab_ajax.js +++ b/app/assets/javascripts/droplab/droplab_ajax.js @@ -45,22 +45,23 @@ require('../window')(function(w){ dynamicList.outerHTML = loadingTemplate.outerHTML; } - this._loadUrlData(config.endpoint).then(function(d) { - if (config.loadingTemplate) { - var dataLoadingTemplate = hook.list.list.querySelector('[data-loading-template]'); + this._loadUrlData(config.endpoint) + .then(function(d) { + if (config.loadingTemplate) { + var dataLoadingTemplate = hook.list.list.querySelector('[data-loading-template]'); - if (dataLoadingTemplate) { - dataLoadingTemplate.outerHTML = self.listTemplate; + if (dataLoadingTemplate) { + dataLoadingTemplate.outerHTML = self.listTemplate; + } } - } - hook.list[config.method].call(hook.list, d); - }).catch(function(e) { - if(e.message) { - console.error(e.message, e.stack); // eslint-disable-line no-console - } else { - console.error(e); // eslint-disable-line no-console - } - }); + hook.list[config.method].call(hook.list, d); + }).catch(function(e) { + if(e.message) { + console.error(e.message, e.stack); // eslint-disable-line no-console + } else { + console.error(e); // eslint-disable-line no-console + } + }); }, destroy: function() { -- cgit v1.2.1 From 91370188aec8ba4f483a1e66f24627abb3df815c Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 9 Jan 2017 17:21:12 -0500 Subject: Update CHANGELOG.md for 8.15.4 [ci skip] --- CHANGELOG.md | 10 ++++++++++ changelogs/unreleased/speed-up-group-milestone-index.yml | 4 ---- 2 files changed, 10 insertions(+), 4 deletions(-) delete mode 100644 changelogs/unreleased/speed-up-group-milestone-index.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index bb7cf9f0ce0..94cc24fd1ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 8.15.4 (2017-01-09) + +- Make successful pipeline emails off for watchers. !8176 +- Speed up group milestone index by passing group_id to IssuesFinder. !8363 +- Don't instrument 405 Grape calls. !8445 +- Update the gitlab-markup gem to the version 1.5.1. !8509 +- Updated Turbolinks to mitigate potential XSS attacks. +- Re-order update steps in the 8.14 -> 8.15 upgrade guide. +- Re-add Google Cloud Storage as a backup strategy. + ## 8.15.3 (2017-01-06) - Rename wiki_events to wiki_page_events in project hooks API to avoid errors. !0 (8425) diff --git a/changelogs/unreleased/speed-up-group-milestone-index.yml b/changelogs/unreleased/speed-up-group-milestone-index.yml deleted file mode 100644 index b5181fa66da..00000000000 --- a/changelogs/unreleased/speed-up-group-milestone-index.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Speed up group milestone index by passing group_id to IssuesFinder -merge_request: 8363 -author: -- cgit v1.2.1 From e0e855b5f49bc8efc3ca69aa83ea28d6becb53cc Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 9 Jan 2017 16:23:51 -0600 Subject: Fix code review suggestions --- app/assets/javascripts/droplab/droplab_ajax.js | 10 +++++----- app/assets/javascripts/lib/utils/common_utils.js.es6 | 2 ++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js index c8850f121d7..f20610b3811 100644 --- a/app/assets/javascripts/droplab/droplab_ajax.js +++ b/app/assets/javascripts/droplab/droplab_ajax.js @@ -3,6 +3,10 @@ /* global droplab */ require('../window')(function(w){ + function droplabAjaxException(message) { + this.message = message; + } + w.droplabAjax = { _loadUrlData: function _loadUrlData(url) { return new Promise(function(resolve, reject) { @@ -56,11 +60,7 @@ require('../window')(function(w){ } hook.list[config.method].call(hook.list, d); }).catch(function(e) { - if(e.message) { - console.error(e.message, e.stack); // eslint-disable-line no-console - } else { - console.error(e); // eslint-disable-line no-console - } + throw new droplabAjaxException(e.message || e); }); }, diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6 index 8aa78f407e5..3e2c75d3cc6 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js.es6 +++ b/app/assets/javascripts/lib/utils/common_utils.js.es6 @@ -169,6 +169,8 @@ w.gl.utils.getParameterByName = (name) => { const url = window.location.href; name = name.replace(/[[\]]/g, '\\$&'); + // Finds the value associated to the name + // Example, state=open where state is the name and open is the value const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`); const results = regex.exec(url); if (!results) return null; -- cgit v1.2.1 From 757bc8ecf43159243d9a0f45f27844f6572e60ac Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 9 Jan 2017 17:41:53 -0600 Subject: Remove duplicate method --- app/assets/javascripts/lib/utils/common_utils.js.es6 | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6 index 3e2c75d3cc6..0c6a3cc3170 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js.es6 +++ b/app/assets/javascripts/lib/utils/common_utils.js.es6 @@ -130,22 +130,6 @@ return window.location.search.slice(1).split('&'); }; - gl.utils.getParameterByName = function(name) { - var url = window.location.href; - var param = name.replace(/[[\]]/g, '\\$&'); - var regex = new RegExp('[?&]' + param + '(=([^&#]*)|&|#|$)'); - var results = regex.exec(url); - - if (!results) { - return null; - } - - if (!results[2]) { - return ''; - } - return decodeURIComponent(results[2].replace(/\+/g, ' ')); - }; - gl.utils.isMetaKey = function(e) { return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; }; @@ -169,8 +153,6 @@ w.gl.utils.getParameterByName = (name) => { const url = window.location.href; name = name.replace(/[[\]]/g, '\\$&'); - // Finds the value associated to the name - // Example, state=open where state is the name and open is the value const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`); const results = regex.exec(url); if (!results) return null; -- cgit v1.2.1 From df3a8d0d0747a8cac936edebc98cf257194b67b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Mon, 9 Jan 2017 21:30:16 +0000 Subject: fix changelog entry for !8425 the author and mr fields got swapped, see https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8425#note_21021254 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb7cf9f0ce0..aca4bbc1f24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ entry. ## 8.15.3 (2017-01-06) -- Rename wiki_events to wiki_page_events in project hooks API to avoid errors. !0 (8425) +- Rename wiki_events to wiki_page_events in project hooks API to avoid errors. !8425 - Rename projects wth reserved names. !8234 - Cache project authorizations even when user has access to zero projects. !8327 - Fix a minor grammar error in merge request widget. !8337 -- cgit v1.2.1 From cff423449f0015d92fa8a06f034eaa2b0ad9af9f Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 10 Jan 2017 14:31:21 +0100 Subject: Fix Rubocop offenses in LDAP related code and spec --- lib/gitlab/ldap/access.rb | 4 ++-- spec/lib/gitlab/ldap/access_spec.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb index 7ed01bf56ca..54a5b1d31cd 100644 --- a/lib/gitlab/ldap/access.rb +++ b/lib/gitlab/ldap/access.rb @@ -69,7 +69,7 @@ module Gitlab user.ldap_block Gitlab::AppLogger.info( - "LDAP account \"#{user.ldap_identity.extern_uid}\" #{reason}, " + + "LDAP account \"#{user.ldap_identity.extern_uid}\" #{reason}, " \ "blocking Gitlab user \"#{user.name}\" (#{user.email})" ) end @@ -78,7 +78,7 @@ module Gitlab user.activate Gitlab::AppLogger.info( - "LDAP account \"#{user.ldap_identity.extern_uid}\" #{reason}, " + + "LDAP account \"#{user.ldap_identity.extern_uid}\" #{reason}, " \ "unblocking Gitlab user \"#{user.name}\" (#{user.email})" ) end diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb index 011c33e63a1..b9d12c3c24c 100644 --- a/spec/lib/gitlab/ldap/access_spec.rb +++ b/spec/lib/gitlab/ldap/access_spec.rb @@ -127,7 +127,7 @@ describe Gitlab::LDAP::Access, lib: true do it 'logs the reason' do expect(Gitlab::AppLogger).to have_received(:info).with( - "LDAP account \"123456\" reason, " + + "LDAP account \"123456\" reason, " \ "blocking Gitlab user \"#{user.name}\" (#{user.email})" ) end @@ -148,7 +148,7 @@ describe Gitlab::LDAP::Access, lib: true do it 'logs the reason' do Gitlab::AppLogger.info( - "LDAP account \"123456\" reason, " + + "LDAP account \"123456\" reason, " \ "unblocking Gitlab user \"#{user.name}\" (#{user.email})" ) end -- cgit v1.2.1 From 292ae1246c9844d3e7dea3b0386106c07df5a149 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 10 Jan 2017 14:31:54 +0100 Subject: Update Gemfile.lock to reflect Rubocop gem version --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index cb71a468aa1..786a44fe0fa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -974,7 +974,7 @@ DEPENDENCIES rqrcode-rails3 (~> 0.1.7) rspec-rails (~> 3.5.0) rspec-retry (~> 0.4.5) - rubocop (~> 0.43.0) + rubocop (~> 0.46.0) rubocop-rspec (~> 1.9.1) ruby-fogbugz (~> 0.2.1) ruby-prof (~> 0.16.2) -- cgit v1.2.1 From 903c4975ae59fbee7e058597e6c8cf87f8147eaa Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Tue, 10 Jan 2017 10:50:14 -0500 Subject: Update CHANGELOG.md for 8.14.6 [ci skip] --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94cc24fd1ea..aea84f92988 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -265,6 +265,11 @@ entry. - Whitelist next project names: help, ci, admin, search. !8227 - Adds back CSS for progress-bars. !8237 +## 8.14.6 (2017-01-10) + +- Update the gitlab-markup gem to the version 1.5.1. !8509 +- Updated Turbolinks to mitigate potential XSS attacks. + ## 8.14.5 (2016-12-14) - Moved Leave Project and Leave Group buttons to access_request_buttons from the settings dropdown. !7600 -- cgit v1.2.1 From fc6b9a10145b69e362eb91052f59a4abf1de051e Mon Sep 17 00:00:00 2001 From: Adam Niedzielski Date: Tue, 10 Jan 2017 17:49:50 +0100 Subject: Do not remove "autodeploy" directory when fetching CI templates --- lib/tasks/gitlab/update_templates.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tasks/gitlab/update_templates.rake b/lib/tasks/gitlab/update_templates.rake index 4f76dad7286..b77a5bb62d1 100644 --- a/lib/tasks/gitlab/update_templates.rake +++ b/lib/tasks/gitlab/update_templates.rake @@ -44,7 +44,7 @@ namespace :gitlab do ), Template.new( "https://gitlab.com/gitlab-org/gitlab-ci-yml.git", - /(\.{1,2}|LICENSE|Pages|\.gitlab-ci.yml)\z/ + /(\.{1,2}|LICENSE|Pages|autodeploy|\.gitlab-ci.yml)\z/ ) ] -- cgit v1.2.1 From 930dd639fb7690246ba29d88aa94a75fe9fd8f2a Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 10 Jan 2017 17:53:09 +0100 Subject: add missing openshift template --- .../autodeploy/OpenShift.gitlab-ci.yml | 76 ++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml diff --git a/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml b/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml new file mode 100644 index 00000000000..249adbc9f4a --- /dev/null +++ b/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml @@ -0,0 +1,76 @@ +# Explaination on the scripts: +# https://gitlab.com/gitlab-examples/openshift-deploy/blob/master/README.md +image: registry.gitlab.com/gitlab-examples/openshift-deploy + +variables: + # Application deployment domain + KUBE_DOMAIN: domain.example.com + +stages: + - build + - test + - review + - staging + - production + +build: + stage: build + script: + - command build + only: + - branches + +production: + stage: production + variables: + CI_ENVIRONMENT_URL: http://production.$KUBE_DOMAIN + script: + - command deploy + environment: + name: production + url: http://production.$KUBE_DOMAIN + when: manual + only: + - master + +staging: + stage: staging + variables: + CI_ENVIRONMENT_URL: http://staging.$KUBE_DOMAIN + script: + - command deploy + environment: + name: staging + url: http://staging.$KUBE_DOMAIN + only: + - master + +review: + stage: review + variables: + CI_ENVIRONMENT_URL: http://$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN + script: + - command deploy + environment: + name: review/$CI_BUILD_REF_NAME + url: http://$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN + on_stop: stop_review + only: + - branches + except: + - master + +stop_review: + stage: review + variables: + GIT_STRATEGY: none + script: + - command destroy + environment: + name: review/$CI_BUILD_REF_NAME + action: stop + when: manual + only: + - branches + except: + - master -- cgit v1.2.1 From 292cf847c9d16214b601990b8cba61fb4963c9e6 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Tue, 10 Jan 2017 11:53:32 -0500 Subject: Update CHANGELOG.md for 8.13.11 [ci skip] --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aea84f92988..d71683bda65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -547,6 +547,11 @@ entry. - Fix "Without projects" filter. !6611 (Ben Bodenmiller) - Fix 404 when visit /projects page +## 8.13.11 (2017-01-10) + +- Update the gitlab-markup gem to the version 1.5.1. !8509 +- Updated Turbolinks to mitigate potential XSS attacks. + ## 8.13.10 (2016-12-14) - API: Memoize the current_user so that sudo can work properly. !8017 -- cgit v1.2.1 From f1e21ae7643ca36a8835873594a0f72915660ec3 Mon Sep 17 00:00:00 2001 From: tauriedavis Date: Mon, 9 Jan 2017 12:52:28 -0800 Subject: 26504 Fix styling of MR jump to discussion button --- app/assets/stylesheets/framework/sidebar.scss | 4 +++- app/assets/stylesheets/pages/notes.scss | 9 +++------ changelogs/unreleased/26504-mr-discussion-btn.yml | 4 ++++ 3 files changed, 10 insertions(+), 7 deletions(-) create mode 100644 changelogs/unreleased/26504-mr-discussion-btn.yml diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index a8641e83154..838f5442fff 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -183,7 +183,9 @@ &.right-sidebar-expanded { .line-resolve-all-container { - display: none; + @media (min-width: $sidebar-breakpoint) { + display: none; + } } } } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index ad4c31ca29e..e2a0253da38 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -526,8 +526,9 @@ ul.notes { } .line-resolve-all { + vertical-align: middle; display: inline-block; - padding: 5px 10px; + padding: 6px 10px; background-color: $gray-light; border: 1px solid $border-color; border-radius: $border-radius-default; @@ -535,18 +536,14 @@ ul.notes { &.has-next-btn { border-top-right-radius: 0; border-bottom-right-radius: 0; + border-right: 0; } .line-resolve-btn { - vertical-align: middle; margin-right: 5px; } } -.line-resolve-text { - vertical-align: middle; -} - .line-resolve-btn { display: inline-block; position: relative; diff --git a/changelogs/unreleased/26504-mr-discussion-btn.yml b/changelogs/unreleased/26504-mr-discussion-btn.yml new file mode 100644 index 00000000000..dec74ec61b1 --- /dev/null +++ b/changelogs/unreleased/26504-mr-discussion-btn.yml @@ -0,0 +1,4 @@ +--- +title: 26504 Fix styling of MR jump to discussion button +merge_request: +author: -- cgit v1.2.1 From f700f6e3cf9b6b7a83f4eac077fa79e0c54332e1 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 5 Jan 2017 18:59:34 +0000 Subject: Merge branch 'patch-turbolinks' into 'security' Updated Turbolinks to patched version of turbolinks-classic See https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/2048 --- Gemfile | 3 +- Gemfile.lock | 10 ++---- vendor/assets/javascripts/jquery.turbolinks.js | 49 ++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 9 deletions(-) create mode 100644 vendor/assets/javascripts/jquery.turbolinks.js diff --git a/Gemfile b/Gemfile index af5566595ed..a8141abafc2 100644 --- a/Gemfile +++ b/Gemfile @@ -221,8 +221,7 @@ gem 'chronic_duration', '~> 0.10.6' gem 'sass-rails', '~> 5.0.6' gem 'coffee-rails', '~> 4.1.0' gem 'uglifier', '~> 2.7.2' -gem 'turbolinks', '~> 2.5.0' -gem 'jquery-turbolinks', '~> 2.1.0' +gem 'gitlab-turbolinks-classic', '~> 2.5', '>= 2.5.6' gem 'addressable', '~> 2.3.8' gem 'bootstrap-sass', '~> 3.3.0' diff --git a/Gemfile.lock b/Gemfile.lock index 786a44fe0fa..bdd591e008a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -264,6 +264,8 @@ GEM mime-types (>= 1.16, < 3) posix-spawn (~> 0.3) gitlab-markup (1.5.1) + gitlab-turbolinks-classic (2.5.6) + coffee-rails gitlab_omniauth-ldap (1.2.1) net-ldap (~> 0.9) omniauth (~> 1.0) @@ -370,9 +372,6 @@ GEM rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - jquery-turbolinks (2.1.0) - railties (>= 3.1.0) - turbolinks jquery-ui-rails (5.0.5) railties (>= 3.2.16) json (1.8.3) @@ -782,8 +781,6 @@ GEM truncato (0.7.8) htmlentities (~> 4.3.1) nokogiri (~> 1.6.1) - turbolinks (2.5.3) - coffee-rails tzinfo (1.2.2) thread_safe (~> 0.1) u2f (0.2.1) @@ -893,6 +890,7 @@ DEPENDENCIES github-linguist (~> 4.7.0) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-markup (~> 1.5.1) + gitlab-turbolinks-classic (~> 2.5, >= 2.5.6) gitlab_omniauth-ldap (~> 1.2.1) gollum-lib (~> 4.2) gollum-rugged_adapter (~> 0.4.2) @@ -911,7 +909,6 @@ DEPENDENCIES jira-ruby (~> 1.1.2) jquery-atwho-rails (~> 1.3.2) jquery-rails (~> 4.1.0) - jquery-turbolinks (~> 2.1.0) jquery-ui-rails (~> 5.0.0) json-schema (~> 2.6.2) jwt @@ -1010,7 +1007,6 @@ DEPENDENCIES thin (~> 1.7.0) timecop (~> 0.8.0) truncato (~> 0.7.8) - turbolinks (~> 2.5.0) u2f (~> 0.2.1) uglifier (~> 2.7.2) underscore-rails (~> 1.8.0) diff --git a/vendor/assets/javascripts/jquery.turbolinks.js b/vendor/assets/javascripts/jquery.turbolinks.js new file mode 100644 index 00000000000..fd6e95e75d5 --- /dev/null +++ b/vendor/assets/javascripts/jquery.turbolinks.js @@ -0,0 +1,49 @@ +// Generated by CoffeeScript 1.7.1 + +/* +jQuery.Turbolinks ~ https://github.com/kossnocorp/jquery.turbolinks +jQuery plugin for drop-in fix binded events problem caused by Turbolinks + +The MIT License +Copyright (c) 2012-2013 Sasha Koss & Rico Sta. Cruz + */ + +(function() { + var $, $document; + + $ = window.jQuery || (typeof require === "function" ? require('jquery') : void 0); + + $document = $(document); + + $.turbo = { + version: '2.1.0', + isReady: false, + use: function(load, fetch) { + return $document.off('.turbo').on("" + load + ".turbo", this.onLoad).on("" + fetch + ".turbo", this.onFetch); + }, + addCallback: function(callback) { + if ($.turbo.isReady) { + callback($); + } + return $document.on('turbo:ready', function() { + return callback($); + }); + }, + onLoad: function() { + $.turbo.isReady = true; + return $document.trigger('turbo:ready'); + }, + onFetch: function() { + return $.turbo.isReady = false; + }, + register: function() { + $(this.onLoad); + return $.fn.ready = this.addCallback; + } + }; + + $.turbo.register(); + + $.turbo.use('page:load', 'page:fetch'); + +}).call(this); -- cgit v1.2.1 From 044a195b1e8ca854e67f8e2782bc69c345bf0df6 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 10 Jan 2017 20:51:57 -0500 Subject: Add comments to issuable finder --- app/finders/issuable_finder.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 5ffaf5ae0f8..1576fc80a6b 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -174,6 +174,7 @@ class IssuableFinder end def no_assignee? + # Assignee_id takes precedence over assignee_username params[:assignee_id] == NONE || params[:assignee_username] == NONE end @@ -199,6 +200,7 @@ class IssuableFinder end def no_author? + # author_id takes precedence over author_username params[:author_id] == NONE || params[:author_username] == NONE end -- cgit v1.2.1 From f68a715366e3ecbdd479e363576842e1a9dfc559 Mon Sep 17 00:00:00 2001 From: Connor Smallman Date: Tue, 3 Jan 2017 19:13:47 +0000 Subject: Removed `hidden-xs` class from alerts Centered alert text and prevented alert actions links from floating right for viewports below medium width (992px) Centered alert text stopped alert actions from floating right for viewports below medium width (992px) Update styles to use correct screen width variable for max-width media query Added unreleased changelog Added class to alert links wrapper Updated styles for new class, float alert links on larger screens --- app/assets/stylesheets/framework/layout.scss | 15 +++++++++++++++ app/views/shared/_no_password.html.haml | 4 ++-- app/views/shared/_no_ssh.html.haml | 4 ++-- .../unreleased/25776-alerts-should-be-responsive.yml | 4 ++++ 4 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/25776-alerts-should-be-responsive.yml diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index 5365b62e456..29d55c44699 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -41,6 +41,21 @@ body { } } + .alert-link-group { + float: right; + } + + /* Center alert text and alert action links on smaller screens */ + @media (max-width: $screen-sm-max) { + .alert { + text-align: center; + } + + .alert-link-group { + float: none; + } + } + /* Stripe the background colors so that adjacent alert-warnings are distinct from one another */ .alert-warning { transition: background-color 0.15s, border-color 0.15s; diff --git a/app/views/shared/_no_password.html.haml b/app/views/shared/_no_password.html.haml index a43bf33751a..ed6fc76c61e 100644 --- a/app/views/shared/_no_password.html.haml +++ b/app/views/shared/_no_password.html.haml @@ -1,8 +1,8 @@ - if cookies[:hide_no_password_message].blank? && !current_user.hide_no_password && current_user.require_password? - .no-password-message.alert.alert-warning.hidden-xs + .no-password-message.alert.alert-warning You won't be able to pull or push project code via #{gitlab_config.protocol.upcase} until you #{link_to 'set a password', edit_profile_password_path} on your account - .pull-right + .alert-link-group = link_to "Don't show again", profile_path(user: {hide_no_password: true}), method: :put | = link_to 'Remind later', '#', class: 'hide-no-password-message' diff --git a/app/views/shared/_no_ssh.html.haml b/app/views/shared/_no_ssh.html.haml index bb5fff2d3bb..d663fa13d10 100644 --- a/app/views/shared/_no_ssh.html.haml +++ b/app/views/shared/_no_ssh.html.haml @@ -1,8 +1,8 @@ - if cookies[:hide_no_ssh_message].blank? && !current_user.hide_no_ssh_key && current_user.require_ssh_key? - .no-ssh-key-message.alert.alert-warning.hidden-xs + .no-ssh-key-message.alert.alert-warning You won't be able to pull or push project code via SSH until you #{link_to 'add an SSH key', profile_keys_path, class: 'alert-link'} to your profile - .pull-right + .alert-link-group = link_to "Don't show again", profile_path(user: {hide_no_ssh_key: true}), method: :put, class: 'alert-link' | = link_to 'Remind later', '#', class: 'hide-no-ssh-message alert-link' diff --git a/changelogs/unreleased/25776-alerts-should-be-responsive.yml b/changelogs/unreleased/25776-alerts-should-be-responsive.yml new file mode 100644 index 00000000000..15006523d3e --- /dev/null +++ b/changelogs/unreleased/25776-alerts-should-be-responsive.yml @@ -0,0 +1,4 @@ +--- +title: Changed alerts to be responsive, centered text on smaller viewports +merge_request: 8424 +author: Connor Smallman -- cgit v1.2.1 From 8a75f68ddec788f10b23467d104987726cf40bc8 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 11 Jan 2017 10:48:29 +0100 Subject: Fix Rubocop YAML config files to avoid CE->EE issues --- .rubocop.yml | 4 - .rubocop_todo.yml | 491 ++++++++++-------------------------------------------- 2 files changed, 85 insertions(+), 410 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 80eb4a5c19e..bf2b2d8afc2 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -343,10 +343,6 @@ Style/ParenthesesAroundCondition: Style/RedundantParentheses: Enabled: true -# Don't use return where it's not required. -Style/RedundantReturn: - Enabled: true - # Don't use semicolons to terminate expressions. Style/Semicolon: Enabled: true diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 2434e8c4b42..6d4d7170fe8 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,19 +1,18 @@ # This configuration was generated by -# `rubocop --auto-gen-config` -# on 2017-01-05 14:31:09 -0800 using RuboCop version 0.46.0. +# `rubocop --auto-gen-config --exclude-limit 0` +# on 2017-01-11 09:38:25 +0000 using RuboCop version 0.46.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 28 +# Offense count: 27 # Configuration parameters: Include. # Include: **/Gemfile, **/gems.rb Bundler/OrderedGems: - Exclude: - - 'Gemfile' + Enabled: false -# Offense count: 174 +# Offense count: 175 Lint/AmbiguousRegexpLiteral: Enabled: false @@ -24,8 +23,7 @@ Lint/AssignmentInCondition: # Offense count: 1 Lint/EmptyWhen: - Exclude: - - 'lib/gitlab/diff/parser.rb' + Enabled: false # Offense count: 20 Lint/HandleExceptions: @@ -33,37 +31,21 @@ Lint/HandleExceptions: # Offense count: 1 Lint/Loop: - Exclude: - - 'app/mailers/notify.rb' + Enabled: false -# Offense count: 28 +# Offense count: 27 Lint/ShadowingOuterLocalVariable: Enabled: false # Offense count: 10 # Cop supports --auto-correct. Lint/UnifiedInteger: - Exclude: - - 'spec/lib/gitlab/metrics/method_call_spec.rb' - - 'spec/lib/gitlab/metrics/metric_spec.rb' - - 'spec/lib/gitlab/metrics/system_spec.rb' - - 'spec/lib/gitlab/metrics/transaction_spec.rb' - - 'spec/models/repository_spec.rb' + Enabled: false # Offense count: 21 # Cop supports --auto-correct. Lint/UnneededSplatExpansion: - Exclude: - - 'config/application.rb' - - 'lib/banzai/filter/gollum_tags_filter.rb' - - 'lib/bitbucket/page.rb' - - 'lib/gitlab/markup_helper.rb' - - 'spec/helpers/auth_helper_spec.rb' - - 'spec/models/project_wiki_spec.rb' - - 'spec/requests/api/projects_spec.rb' - - 'spec/requests/api/tags_spec.rb' - - 'spec/support/seed_helper.rb' - - 'spec/support/test_env.rb' + Enabled: false # Offense count: 82 # Cop supports --auto-correct. @@ -77,28 +59,20 @@ Lint/UnusedBlockArgument: Lint/UnusedMethodArgument: Enabled: false -# Offense count: 94 +# Offense count: 93 # Configuration parameters: CountComments. Metrics/BlockLength: - Max: 289 + Max: 288 # Offense count: 3 # Cop supports --auto-correct. Performance/RedundantBlockCall: - Exclude: - - 'app/controllers/application_controller.rb' - - 'lib/gitlab/backend/shell.rb' - - 'lib/gitlab/optimistic_locking.rb' + Enabled: false # Offense count: 5 # Cop supports --auto-correct. Performance/RedundantMatch: - Exclude: - - 'app/models/external_issue.rb' - - 'lib/ci/api/helpers.rb' - - 'lib/extracts_path.rb' - - 'lib/gitlab/diff/highlight.rb' - - 'lib/gitlab/diff/parser.rb' + Enabled: false # Offense count: 32 # Cop supports --auto-correct. @@ -108,26 +82,16 @@ Performance/RedundantMerge: # Offense count: 7 RSpec/BeEql: - Exclude: - - 'spec/lib/gitlab/ldap/user_spec.rb' - - 'spec/lib/gitlab/o_auth/user_spec.rb' - - 'spec/lib/gitlab/saml/user_spec.rb' + Enabled: false # Offense count: 15 # Configuration parameters: CustomIncludeMethods. RSpec/EmptyExampleGroup: - Exclude: - - 'spec/features/issues_spec.rb' - - 'spec/features/notes_on_merge_requests_spec.rb' - - 'spec/lib/gitlab/git_access_spec.rb' - - 'spec/models/project_snippet_spec.rb' - - 'spec/models/protected_branch_spec.rb' - - 'spec/routing/routing_spec.rb' + Enabled: false # Offense count: 24 RSpec/ExpectActual: - Exclude: - - 'spec/lib/gitlab/regex_spec.rb' + Enabled: false # Offense count: 58 # Configuration parameters: EnforcedStyle, SupportedStyles. @@ -139,74 +103,48 @@ RSpec/HookArgument: # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: is_expected, should RSpec/ImplicitExpect: - Exclude: - - 'spec/lib/banzai/renderer_spec.rb' - - 'spec/models/project_group_link_spec.rb' - - 'spec/models/project_services/external_wiki_service_spec.rb' - - 'spec/models/project_spec.rb' + Enabled: false -# Offense count: 235 +# Offense count: 237 RSpec/LeadingSubject: Enabled: false -# Offense count: 246 +# Offense count: 253 RSpec/LetSetup: Enabled: false # Offense count: 13 RSpec/MessageChain: - Exclude: - - 'spec/finders/move_to_project_finder_spec.rb' - - 'spec/helpers/issues_helper_spec.rb' - - 'spec/initializers/secret_token_spec.rb' - - 'spec/models/build_spec.rb' - - 'spec/requests/api/internal_spec.rb' - - 'spec/services/merge_requests/build_service_spec.rb' - - 'spec/services/notes/create_service_spec.rb' - - 'spec/workers/emails_on_push_worker_spec.rb' - -# Offense count: 468 + Enabled: false + +# Offense count: 479 # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: have_received, receive RSpec/MessageSpies: Enabled: false -# Offense count: 3013 +# Offense count: 3036 RSpec/MultipleExpectations: Max: 37 -# Offense count: 2072 +# Offense count: 2133 RSpec/NamedSubject: Enabled: false -# Offense count: 1949 +# Offense count: 1974 # Configuration parameters: MaxNesting. RSpec/NestedGroups: Enabled: false # Offense count: 32 RSpec/RepeatedDescription: - Exclude: - - 'spec/controllers/projects/todo_controller_spec.rb' - - 'spec/features/boards/boards_spec.rb' - - 'spec/helpers/issuables_helper_spec.rb' - - 'spec/lib/banzai/filter/emoji_filter_spec.rb' - - 'spec/models/commit_range_spec.rb' - - 'spec/models/global_milestone_spec.rb' - - 'spec/models/hooks/system_hook_spec.rb' - - 'spec/requests/api/commits_spec.rb' - - 'spec/requests/api/merge_requests_spec.rb' - - 'spec/requests/api/users_spec.rb' - - 'spec/requests/lfs_http_spec.rb' - - 'spec/routing/admin_routing_spec.rb' - - 'spec/services/notification_service_spec.rb' + Enabled: false # Offense count: 1 RSpec/SingleArgumentMessageChain: - Exclude: - - 'spec/requests/api/internal_spec.rb' + Enabled: false -# Offense count: 125 +# Offense count: 133 RSpec/SubjectStub: Enabled: false @@ -217,7 +155,7 @@ RSpec/SubjectStub: Rails/DynamicFindBy: Enabled: false -# Offense count: 929 +# Offense count: 932 # Cop supports --auto-correct. # Configuration parameters: Include. # Include: spec/**/*, test/**/* @@ -228,7 +166,7 @@ Rails/HttpPositionalArguments: Rails/OutputSafety: Enabled: false -# Offense count: 181 +# Offense count: 182 # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: strict, flexible Rails/TimeZone: @@ -239,31 +177,15 @@ Rails/TimeZone: # Configuration parameters: Include. # Include: app/models/**/*.rb Rails/Validation: - Exclude: - - 'app/models/ci/build.rb' - - 'app/models/ci/pipeline.rb' - - 'app/models/ci/runner_project.rb' - - 'app/models/ci/trigger.rb' - - 'app/models/commit_status.rb' - - 'app/models/members/group_member.rb' - - 'app/models/members/project_member.rb' - - 'app/models/project.rb' - - 'app/models/protected_branch.rb' - - 'app/models/user.rb' + Enabled: false # Offense count: 8 # Cop supports --auto-correct. # Configuration parameters: AutoCorrect. Security/JSONLoad: - Exclude: - - 'db/migrate/20161019190736_migrate_sidekiq_queues_from_default.rb' - - 'db/migrate/20161024042317_migrate_mailroom_queue_from_default.rb' - - 'db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb' - - 'spec/migrations/migrate_process_commit_worker_jobs_spec.rb' - - 'spec/models/project_services/irker_service_spec.rb' - - 'spec/support/stub_gitlab_calls.rb' + Enabled: false -# Offense count: 339 +# Offense count: 346 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. # SupportedStyles: with_first_parameter, with_fixed_indentation @@ -277,14 +199,14 @@ Style/AlignParameters: Style/AndOr: Enabled: false -# Offense count: 53 +# Offense count: 54 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: percent_q, bare_percent Style/BarePercentLiterals: Enabled: false -# Offense count: 354 +# Offense count: 358 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: braces, no_braces, context_dependent @@ -293,13 +215,7 @@ Style/BracesAroundHashParameters: # Offense count: 6 Style/CaseEquality: - Exclude: - - 'app/helpers/auth_helper.rb' - - 'app/models/commit.rb' - - 'app/models/protected_branch.rb' - - 'app/services/projects/download_service.rb' - - 'config/initializers/trusted_proxies.rb' - - 'spec/features/calendar_spec.rb' + Enabled: false # Offense count: 37 # Cop supports --auto-correct. @@ -311,11 +227,7 @@ Style/ColonMethodCall: # Configuration parameters: Keywords. # Keywords: TODO, FIXME, OPTIMIZE, HACK, REVIEW Style/CommentAnnotation: - Exclude: - - 'config/routes/user.rb' - - 'lib/api/commits.rb' - - 'lib/api/entities.rb' - - 'spec/requests/api/project_snippets_spec.rb' + Enabled: false # Offense count: 29 # Cop supports --auto-correct. @@ -333,33 +245,12 @@ Style/DotPosition: # Offense count: 18 Style/DoubleNegation: - Exclude: - - 'app/controllers/users_controller.rb' - - 'app/models/commit.rb' - - 'app/models/merge_request.rb' - - 'app/models/project.rb' - - 'app/models/project_services/buildkite_service.rb' - - 'app/models/project_services/drone_ci_service.rb' - - 'app/models/project_wiki.rb' - - 'app/models/user.rb' - - 'config/initializers/6_validations.rb' - - 'lib/banzai/filter/relative_link_filter.rb' - - 'lib/gitlab/git/diff_collection.rb' - - 'lib/gitlab/git/repository.rb' - - 'lib/gitlab/sherlock.rb' - - 'lib/gitlab/sherlock/middleware.rb' + Enabled: false # Offense count: 7 # Cop supports --auto-correct. Style/EachWithObject: - Exclude: - - 'app/controllers/projects/cycle_analytics_controller.rb' - - 'app/services/merge_requests/get_urls_service.rb' - - 'lib/ci/ansi2html.rb' - - 'lib/expand_variables.rb' - - 'lib/gitlab/ee_compat_check.rb' - - 'lib/gitlab/import_export/members_mapper.rb' - - 'lib/gitlab/import_export/relation_factory.rb' + Enabled: false # Offense count: 24 # Cop supports --auto-correct. @@ -371,11 +262,7 @@ Style/EmptyElse: # Offense count: 4 # Cop supports --auto-correct. Style/EmptyLiteral: - Exclude: - - 'features/steps/project/commits/commits.rb' - - 'lib/gitlab/fogbugz_import/importer.rb' - - 'lib/gitlab/git/diff_collection.rb' - - 'spec/lib/gitlab/workhorse_spec.rb' + Enabled: false # Offense count: 57 # Cop supports --auto-correct. @@ -384,7 +271,7 @@ Style/EmptyLiteral: Style/EmptyMethod: Enabled: false -# Offense count: 145 +# Offense count: 147 # Cop supports --auto-correct. # Configuration parameters: AllowForAlignment, ForceEqualSignAlignment. Style/ExtraSpacing: @@ -394,33 +281,16 @@ Style/ExtraSpacing: # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: format, sprintf, percent Style/FormatString: - Exclude: - - 'app/models/ci/pipeline.rb' - - 'app/services/gravatar_service.rb' - - 'config/initializers/rack_lineprof.rb' - - 'lib/gitlab/version_info.rb' - - 'spec/factories/lfs_objects.rb' - - 'spec/services/issues/move_service_spec.rb' - -# Offense count: 236 + Enabled: false + +# Offense count: 238 # Configuration parameters: MinBodyLength. Style/GuardClause: Enabled: false # Offense count: 11 Style/IfInsideElse: - Exclude: - - 'app/controllers/projects/forks_controller.rb' - - 'app/controllers/projects/imports_controller.rb' - - 'app/helpers/milestones_helper.rb' - - 'app/helpers/projects_helper.rb' - - 'app/services/system_note_service.rb' - - 'config/initializers/1_settings.rb' - - 'lib/gitlab/auth.rb' - - 'lib/gitlab/checks/change_access.rb' - - 'lib/gitlab/database.rb' - - 'spec/controllers/projects/merge_requests_controller_spec.rb' - - 'spec/support/import_export/export_file_helper.rb' + Enabled: false # Offense count: 173 # Cop supports --auto-correct. @@ -435,7 +305,7 @@ Style/IfUnlessModifier: Style/IndentArray: Enabled: false -# Offense count: 99 +# Offense count: 101 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. # SupportedStyles: special_inside_parentheses, consistent, align_braces @@ -452,49 +322,23 @@ Style/Lambda: # Offense count: 5 # Cop supports --auto-correct. Style/LineEndConcatenation: - Exclude: - - 'app/helpers/preferences_helper.rb' - - 'app/helpers/tree_helper.rb' - - 'app/models/merge_request.rb' - - 'spec/lib/gitlab/gfm/reference_rewriter_spec.rb' + Enabled: false # Offense count: 19 # Cop supports --auto-correct. Style/MethodCallParentheses: - Exclude: - - 'lib/ci/ansi2html.rb' - - 'spec/controllers/dashboard/todos_controller_spec.rb' - - 'spec/controllers/projects/issues_controller_spec.rb' - - 'spec/controllers/projects/merge_requests_controller_spec.rb' - - 'spec/controllers/projects/snippets_controller_spec.rb' - - 'spec/features/calendar_spec.rb' - - 'spec/features/dashboard/datetime_on_tooltips_spec.rb' - - 'spec/helpers/submodule_helper_spec.rb' - - 'spec/lib/gitlab/git/repository_spec.rb' - - 'spec/requests/api/internal_spec.rb' + Enabled: false # Offense count: 9 Style/MethodMissing: - Exclude: - - 'app/controllers/projects/application_controller.rb' - - 'app/models/commit.rb' - - 'app/models/network/commit.rb' - - 'app/models/repository.rb' - - 'config/initializers/gollum.rb' - - 'lib/bitbucket/collection.rb' - - 'lib/gitlab/github_import/client.rb' - - 'lib/gitlab/lazy.rb' - - 'lib/uploaded_file.rb' + Enabled: false # Offense count: 3 # Cop supports --auto-correct. Style/MultilineIfModifier: - Exclude: - - 'app/helpers/snippets_helper.rb' - - 'app/models/merge_request.rb' - - 'app/models/project_wiki.rb' + Enabled: false -# Offense count: 178 +# Offense count: 179 # Cop supports --auto-correct. Style/MutableConstant: Enabled: false @@ -502,49 +346,21 @@ Style/MutableConstant: # Offense count: 8 # Cop supports --auto-correct. Style/NestedParenthesizedCalls: - Exclude: - - 'app/helpers/commits_helper.rb' - - 'app/workers/irker_worker.rb' - - 'spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb' - - 'spec/lib/gitlab/email/message/repository_push_spec.rb' + Enabled: false # Offense count: 13 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles. # SupportedStyles: skip_modifier_ifs, always Style/Next: - Exclude: - - 'app/models/namespace.rb' - - 'app/models/user.rb' - - 'config/initializers/secret_token.rb' - - 'db/migrate/20161220141214_remove_dot_git_from_group_names.rb' - - 'db/migrate/20161226122833_remove_dot_git_from_usernames.rb' - - 'lib/banzai/filter/math_filter.rb' - - 'lib/banzai/filter/table_of_contents_filter.rb' - - 'lib/gitlab/fogbugz_import/importer.rb' - - 'lib/gitlab/metrics/instrumentation.rb' - - 'lib/gitlab/metrics/sampler.rb' - - 'lib/gitlab/project_search_results.rb' + Enabled: false # Offense count: 19 # Cop supports --auto-correct. # Configuration parameters: EnforcedOctalStyle, SupportedOctalStyles. # SupportedOctalStyles: zero_with_o, zero_only Style/NumericLiteralPrefix: - Exclude: - - 'config/initializers/1_settings.rb' - - 'config/initializers/secret_token.rb' - - 'lib/gitlab/backend/shell.rb' - - 'lib/gitlab/import_export/command_line_util.rb' - - 'lib/gitlab/workhorse.rb' - - 'spec/features/projects/import_export/export_file_spec.rb' - - 'spec/lib/gitlab/git/blob_spec.rb' - - 'spec/lib/gitlab/git/hook_spec.rb' - - 'spec/lib/gitlab/git/repository_spec.rb' - - 'spec/lib/gitlab/workhorse_spec.rb' - - 'spec/support/import_export/export_file_helper.rb' - - 'spec/support/test_env.rb' - - 'spec/tasks/gitlab/backup_rake_spec.rb' + Enabled: false # Offense count: 19 # Cop supports --auto-correct. @@ -558,7 +374,7 @@ Style/NumericPredicate: Style/ParallelAssignment: Enabled: false -# Offense count: 416 +# Offense count: 417 # Cop supports --auto-correct. # Configuration parameters: PreferredDelimiters. Style/PercentLiteralDelimiters: @@ -569,28 +385,14 @@ Style/PercentLiteralDelimiters: # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: lower_case_q, upper_case_q Style/PercentQLiterals: - Exclude: - - 'spec/lib/gitlab/diff/highlight_spec.rb' - - 'spec/models/project_services/bamboo_service_spec.rb' - - 'spec/models/project_services/teamcity_service_spec.rb' - - 'spec/workers/repository_import_worker_spec.rb' + Enabled: false # Offense count: 13 # Cop supports --auto-correct. Style/PerlBackrefs: - Exclude: - - 'app/controllers/projects/application_controller.rb' - - 'app/helpers/submodule_helper.rb' - - 'lib/banzai/filter/abstract_reference_filter.rb' - - 'lib/banzai/filter/autolink_filter.rb' - - 'lib/banzai/filter/emoji_filter.rb' - - 'lib/banzai/filter/gollum_tags_filter.rb' - - 'lib/expand_variables.rb' - - 'lib/gitlab/diff/highlight.rb' - - 'lib/gitlab/search_results.rb' - - 'lib/gitlab/sherlock/query.rb' - -# Offense count: 63 + Enabled: false + +# Offense count: 64 # Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist. # NamePrefix: is_, has_, have_ # NamePrefixBlacklist: is_, has_, have_ @@ -608,13 +410,7 @@ Style/PreferredHashMethods: # Offense count: 8 # Cop supports --auto-correct. Style/Proc: - Exclude: - - 'app/mailers/base_mailer.rb' - - 'app/models/milestone.rb' - - 'app/models/service.rb' - - 'lib/api/api_guard.rb' - - 'lib/gitlab/ldap/config.rb' - - 'spec/initializers/trusted_proxies_spec.rb' + Enabled: false # Offense count: 50 # Cop supports --auto-correct. @@ -626,47 +422,23 @@ Style/RaiseArgs: # Offense count: 4 # Cop supports --auto-correct. Style/RedundantBegin: - Exclude: - - 'app/models/ci/build.rb' - - 'app/models/merge_request.rb' - - 'app/services/projects/import_service.rb' - - 'lib/gitlab/git/commit.rb' + Enabled: false # Offense count: 1 # Cop supports --auto-correct. Style/RedundantException: - Exclude: - - 'app/helpers/preferences_helper.rb' + Enabled: false # Offense count: 29 # Cop supports --auto-correct. Style/RedundantFreeze: - Exclude: - - 'app/models/merge_request.rb' - - 'app/validators/color_validator.rb' - - 'app/validators/line_code_validator.rb' - - 'lib/api/templates.rb' - - 'lib/banzai/filter/blockquote_fence_filter.rb' - - 'lib/banzai/filter/gollum_tags_filter.rb' - - 'lib/banzai/filter/yaml_front_matter_filter.rb' - - 'lib/gitlab/diff/inline_diff.rb' - - 'lib/gitlab/ee_compat_check.rb' - - 'lib/gitlab/import_export/relation_factory.rb' - - 'lib/gitlab/incoming_email.rb' - - 'lib/gitlab/middleware/static.rb' - - 'lib/gitlab/regex.rb' + Enabled: false # Offense count: 11 # Cop supports --auto-correct. # Configuration parameters: AllowMultipleReturnValues. Style/RedundantReturn: - Exclude: - - 'app/controllers/concerns/issuable_actions.rb' - - 'app/controllers/groups/application_controller.rb' - - 'app/controllers/groups_controller.rb' - - 'app/controllers/projects/labels_controller.rb' - - 'app/controllers/projects/milestones_controller.rb' - - 'app/services/access_token_validation_service.rb' + Enabled: false # Offense count: 359 # Cop supports --auto-correct. @@ -683,49 +455,26 @@ Style/RegexpLiteral: # Offense count: 19 # Cop supports --auto-correct. Style/RescueModifier: - Exclude: - - 'app/controllers/admin/system_info_controller.rb' - - 'app/controllers/users_controller.rb' - - 'app/helpers/blob_helper.rb' - - 'app/helpers/issues_helper.rb' - - 'app/models/diff_note.rb' - - 'app/models/project_services/external_wiki_service.rb' - - 'app/models/sent_notification.rb' - - 'app/models/todo.rb' - - 'config/initializers/1_settings.rb' - - 'config/initializers/attr_encrypted_no_db_connection.rb' - - 'lib/gitlab/current_settings.rb' - - 'lib/gitlab/diff/parser.rb' - - 'lib/gitlab/kubernetes.rb' - - 'lib/gitlab/slash_commands/command_definition.rb' - - 'spec/support/matchers/be_url.rb' + Enabled: false # Offense count: 7 # Cop supports --auto-correct. Style/SelfAssignment: - Exclude: - - 'app/models/project_team.rb' - - 'app/services/notification_service.rb' - - 'lib/api/runners.rb' - - 'spec/features/merge_requests/diff_notes_resolve_spec.rb' - - 'spec/support/import_export/configuration_helper.rb' + Enabled: false # Offense count: 2 # Configuration parameters: Methods. # Methods: {"reduce"=>["acc", "elem"]}, {"inject"=>["acc", "elem"]} Style/SingleLineBlockParams: - Exclude: - - 'app/models/commit.rb' - - 'spec/support/services_shared_context.rb' + Enabled: false # Offense count: 50 # Cop supports --auto-correct. # Configuration parameters: AllowIfMethodIsEmpty. Style/SingleLineMethods: - Exclude: - - 'lib/ci/ansi2html.rb' + Enabled: false -# Offense count: 140 +# Offense count: 138 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: space, no_space @@ -736,33 +485,14 @@ Style/SpaceBeforeBlockBraces: # Cop supports --auto-correct. # Configuration parameters: AllowForAlignment. Style/SpaceBeforeFirstArg: - Exclude: - - 'config/routes/project.rb' - - 'features/steps/project/source/browse_files.rb' - - 'features/steps/project/source/markdown_render.rb' - - 'spec/routing/project_routing_spec.rb' - - 'spec/services/system_note_service_spec.rb' + Enabled: false # Offense count: 37 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: require_no_space, require_space Style/SpaceInLambdaLiteral: - Exclude: - - 'app/models/concerns/mentionable.rb' - - 'app/models/issue.rb' - - 'app/models/member.rb' - - 'lib/api/entities.rb' - - 'lib/file_size_validator.rb' - - 'lib/gitlab/visibility_level.rb' - - 'spec/models/concerns/participable_spec.rb' - - 'spec/models/cycle_analytics/code_spec.rb' - - 'spec/models/cycle_analytics/issue_spec.rb' - - 'spec/models/cycle_analytics/plan_spec.rb' - - 'spec/models/cycle_analytics/production_spec.rb' - - 'spec/models/cycle_analytics/review_spec.rb' - - 'spec/models/cycle_analytics/staging_spec.rb' - - 'spec/models/cycle_analytics/test_spec.rb' + Enabled: false # Offense count: 174 # Cop supports --auto-correct. @@ -784,8 +514,7 @@ Style/SpaceInsideParens: # Offense count: 4 # Cop supports --auto-correct. Style/SpaceInsidePercentLiteralDelimiters: - Exclude: - - 'lib/gitlab/git_access.rb' + Enabled: false # Offense count: 53 # Cop supports --auto-correct. @@ -799,23 +528,9 @@ Style/SpecialGlobalVars: # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: single_quotes, double_quotes Style/StringLiteralsInInterpolation: - Exclude: - - 'app/helpers/labels_helper.rb' - - 'app/helpers/time_helper.rb' - - 'app/helpers/todos_helper.rb' - - 'app/models/application_setting.rb' - - 'app/models/project_services/campfire_service.rb' - - 'app/models/project_services/jira_service.rb' - - 'db/migrate/20161017125927_add_unique_index_to_labels.rb' - - 'features/steps/groups.rb' - - 'features/steps/project/merge_requests.rb' - - 'lib/banzai/filter/abstract_reference_filter.rb' - - 'lib/ci/gitlab_ci_yaml_processor.rb' - - 'lib/gitlab/google_code_import/importer.rb' - - 'spec/lib/banzai/filter/issue_reference_filter_spec.rb' - - 'spec/models/project_services/hipchat_service_spec.rb' - -# Offense count: 52 + Enabled: false + +# Offense count: 54 # Cop supports --auto-correct. # Configuration parameters: IgnoredMethods. # IgnoredMethods: respond_to, define_method @@ -827,21 +542,16 @@ Style/SymbolProc: # Configuration parameters: EnforcedStyle, SupportedStyles, AllowSafeAssignment. # SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex Style/TernaryParentheses: - Exclude: - - 'features/support/capybara.rb' - - 'lib/api/projects.rb' - - 'lib/banzai/filter/table_of_contents_filter.rb' - - 'lib/gitlab/ci/build/artifacts/metadata/entry.rb' - - 'spec/support/capybara.rb' + Enabled: false -# Offense count: 35 +# Offense count: 36 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyleForMultiline, SupportedStyles. # SupportedStyles: comma, consistent_comma, no_comma Style/TrailingCommaInArguments: Enabled: false -# Offense count: 149 +# Offense count: 150 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyleForMultiline, SupportedStyles. # SupportedStyles: comma, consistent_comma, no_comma @@ -852,15 +562,9 @@ Style/TrailingCommaInLiteral: # Cop supports --auto-correct. # Configuration parameters: AllowNamedUnderscoreVariables. Style/TrailingUnderscoreVariable: - Exclude: - - 'app/controllers/admin/background_jobs_controller.rb' - - 'app/controllers/invites_controller.rb' - - 'app/controllers/projects/git_http_client_controller.rb' - - 'app/helpers/tab_helper.rb' - - 'lib/gitlab/git/repository.rb' - - 'lib/gitlab/logger.rb' + Enabled: false -# Offense count: 66 +# Offense count: 67 # Cop supports --auto-correct. Style/TrailingWhitespace: Enabled: false @@ -870,44 +574,19 @@ Style/TrailingWhitespace: # Configuration parameters: ExactNameMatch, AllowPredicates, AllowDSLWriters, IgnoreClassMethods, Whitelist. # Whitelist: to_ary, to_a, to_c, to_enum, to_h, to_hash, to_i, to_int, to_io, to_open, to_path, to_proc, to_r, to_regexp, to_str, to_s, to_sym Style/TrivialAccessors: - Exclude: - - 'app/models/external_issue.rb' - - 'app/serializers/base_serializer.rb' - - 'lib/gitlab/ldap/person.rb' + Enabled: false # Offense count: 2 # Cop supports --auto-correct. Style/UnlessElse: - Exclude: - - 'lib/gitlab/project_search_results.rb' - - 'spec/features/issues/award_emoji_spec.rb' + Enabled: false # Offense count: 17 # Cop supports --auto-correct. Style/UnneededInterpolation: - Exclude: - - 'app/helpers/boards_helper.rb' - - 'app/helpers/projects_helper.rb' - - 'app/helpers/search_helper.rb' - - 'app/helpers/time_helper.rb' - - 'app/models/merge_request.rb' - - 'app/models/project_services/pushover_service.rb' - - 'features/steps/project/forked_merge_requests.rb' - - 'spec/features/admin/admin_projects_spec.rb' - - 'spec/features/issues_spec.rb' - - 'spec/features/projects/branches_spec.rb' - - 'spec/features/projects/issuable_templates_spec.rb' - - 'spec/lib/banzai/filter/issue_reference_filter_spec.rb' - - 'spec/views/projects/commit/_commit_box.html.haml_spec.rb' + Enabled: false # Offense count: 8 # Cop supports --auto-correct. Style/ZeroLengthPredicate: - Exclude: - - 'app/models/deploy_key.rb' - - 'app/models/merge_request.rb' - - 'app/models/network/commit.rb' - - 'app/models/network/graph.rb' - - 'app/models/project_services/asana_service.rb' - - 'app/models/repository.rb' - - 'lib/extracts_path.rb' + Enabled: false -- cgit v1.2.1 From d3cfa39e3cb7a4a04f3f42d00c4740d317690bbc Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 21 Jul 2016 11:40:49 +0200 Subject: New error message recreating projects on pending delete --- app/models/concerns/valid_attribute.rb | 10 ++++++++ app/models/project.rb | 20 +++++++++++++++ .../unreleased/fix-project-delete-tooltip.yml | 4 +++ spec/models/project_spec.rb | 30 ++++++++++++++++++---- 4 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 app/models/concerns/valid_attribute.rb create mode 100644 changelogs/unreleased/fix-project-delete-tooltip.yml diff --git a/app/models/concerns/valid_attribute.rb b/app/models/concerns/valid_attribute.rb new file mode 100644 index 00000000000..8c35cea8d58 --- /dev/null +++ b/app/models/concerns/valid_attribute.rb @@ -0,0 +1,10 @@ +module ValidAttribute + extend ActiveSupport::Concern + + # Checks whether an attribute has failed validation or not + # + # +attribute+ The symbolised name of the attribute i.e :name + def valid_attribute?(attribute) + self.errors.empty? || self.errors.messages[attribute].nil? + end +end diff --git a/app/models/project.rb b/app/models/project.rb index 94a6f3ba799..c22386c84e9 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -12,6 +12,7 @@ class Project < ActiveRecord::Base include AfterCommitQueue include CaseSensitivity include TokenAuthenticatable + include ValidAttribute include ProjectFeaturesCompatibility include SelectForProjectAuthorization include Routable @@ -65,6 +66,8 @@ class Project < ActiveRecord::Base end end + after_validation :check_pending_delete + ActsAsTaggableOn.strict_case_match = true acts_as_taggable_on :tags @@ -1320,4 +1323,21 @@ class Project < ActiveRecord::Base stats = statistics || build_statistics stats.update(namespace_id: namespace_id) end + + def check_pending_delete + return if valid_attribute?(:name) && valid_attribute?(:path) + return unless pending_delete_twin + + %i[route route.path name path].each do |error| + errors.delete(error) + end + + errors.add(:base, "The project is still being deleted. Please try again later.") + end + + def pending_delete_twin + return false unless path + + Project.unscoped.where(pending_delete: true).find_with_namespace(path_with_namespace) + end end diff --git a/changelogs/unreleased/fix-project-delete-tooltip.yml b/changelogs/unreleased/fix-project-delete-tooltip.yml new file mode 100644 index 00000000000..42fd9c32519 --- /dev/null +++ b/changelogs/unreleased/fix-project-delete-tooltip.yml @@ -0,0 +1,4 @@ +--- +title: Fix project queued for deletion re-creation tooltip +merge_request: +author: diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 3ec7bb46686..32779eb92ef 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -190,34 +190,54 @@ describe Project, models: true do end it 'does not allow an invalid URI as import_url' do - project2 = build(:project, import_url: 'invalid://') + project2 = build(:empty_project, import_url: 'invalid://') expect(project2).not_to be_valid end it 'does allow a valid URI as import_url' do - project2 = build(:project, import_url: 'ssh://test@gitlab.com/project.git') + project2 = build(:empty_project, import_url: 'ssh://test@gitlab.com/project.git') expect(project2).to be_valid end it 'allows an empty URI' do - project2 = build(:project, import_url: '') + project2 = build(:empty_project, import_url: '') expect(project2).to be_valid end it 'does not produce import data on an empty URI' do - project2 = build(:project, import_url: '') + project2 = build(:empty_project, import_url: '') expect(project2.import_data).to be_nil end it 'does not produce import data on an invalid URI' do - project2 = build(:project, import_url: 'test://') + project2 = build(:empty_project, import_url: 'test://') expect(project2.import_data).to be_nil end + + describe 'project pending deletion' do + let!(:project_pending_deletion) do + create(:empty_project, + pending_delete: true) + end + let(:new_project) do + build(:empty_project, + name: project_pending_deletion.name, + namespace: project_pending_deletion.namespace) + end + + before do + new_project.validate + end + + it 'contains errors related to the project being deleted' do + expect(new_project.errors.full_messages.first).to eq('The project is still being deleted. Please try again later.') + end + end end describe 'default_scope' do -- cgit v1.2.1 From 72c68b98ae5e016254e56afc60eb9fa662a452de Mon Sep 17 00:00:00 2001 From: Mark Fletcher Date: Wed, 11 Jan 2017 14:38:41 +0000 Subject: Fix Users API to accept confirm parameter --- changelogs/unreleased/fix-user-api-confirm-param.yml | 4 ++++ lib/api/users.rb | 2 +- spec/requests/api/users_spec.rb | 9 +++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/fix-user-api-confirm-param.yml diff --git a/changelogs/unreleased/fix-user-api-confirm-param.yml b/changelogs/unreleased/fix-user-api-confirm-param.yml new file mode 100644 index 00000000000..42642576634 --- /dev/null +++ b/changelogs/unreleased/fix-user-api-confirm-param.yml @@ -0,0 +1,4 @@ +--- +title: Fix 500 error when POSTing to Users API with optional confirm param +merge_request: +author: diff --git a/lib/api/users.rb b/lib/api/users.rb index 0db76ec7877..11a7368b4c0 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -93,7 +93,7 @@ module API # Filter out params which are used later user_params = declared_params(include_missing: false) identity_attrs = user_params.slice(:provider, :extern_uid) - confirm = params.delete(:confirm) + confirm = user_params.delete(:confirm) user = User.new(user_params.except(:extern_uid, :provider)) user.skip_confirmation! unless confirm diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 2c2e17eddb0..5bf5bf0739e 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -137,6 +137,15 @@ describe API::Users, api: true do expect(new_user.can_create_group).to eq(true) end + it "creates user with optional attributes" do + optional_attributes = { confirm: true } + attributes = attributes_for(:user).merge(optional_attributes) + + post api('/users', admin), attributes + + expect(response).to have_http_status(201) + end + it "creates non-admin user" do post api('/users', admin), attributes_for(:user, admin: false, can_create_group: false) expect(response).to have_http_status(201) -- cgit v1.2.1 From 9c27408bb75f7228a015e3975cc7bcd38f9dec30 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Wed, 11 Jan 2017 09:53:19 -0500 Subject: Fix broken link in docs [ci skip] --- doc/project_services/slack_slash_commands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/project_services/slack_slash_commands.md b/doc/project_services/slack_slash_commands.md index b6b5c741d90..d9ff573d185 100644 --- a/doc/project_services/slack_slash_commands.md +++ b/doc/project_services/slack_slash_commands.md @@ -6,7 +6,7 @@ Slack commands give users an extra interface to perform common operations from the chat environment. This allows one to, for example, create an issue as soon as the idea was discussed in chat. For all available commands try the help subcommand, for example: `/gitlab help`, -all review the [full list of commands](../integrations/chat_commands.md). +all review the [full list of commands](../integration/chat_commands.md). ## Prerequisites -- cgit v1.2.1 From 63bd49696df1520f5d47daa8ffc8e0f5589f737e Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 11 Jan 2017 09:59:33 -0500 Subject: Remove more orphans when removing stray namespaces Fixes https://gitlab.com/gitlab-org/gitlab-ce/issues/25146 --- .../fix-more-orphans-remove-undeleted-groups.yml | 4 + .../20161117114805_remove_undeleted_groups.rb | 96 ++++++++++++++++------ 2 files changed, 76 insertions(+), 24 deletions(-) create mode 100644 changelogs/unreleased/fix-more-orphans-remove-undeleted-groups.yml diff --git a/changelogs/unreleased/fix-more-orphans-remove-undeleted-groups.yml b/changelogs/unreleased/fix-more-orphans-remove-undeleted-groups.yml new file mode 100644 index 00000000000..bc2068b8177 --- /dev/null +++ b/changelogs/unreleased/fix-more-orphans-remove-undeleted-groups.yml @@ -0,0 +1,4 @@ +--- +title: Remove extra orphaned rows when removing stray namespaces +merge_request: 7841 +author: diff --git a/db/migrate/20161117114805_remove_undeleted_groups.rb b/db/migrate/20161117114805_remove_undeleted_groups.rb index 696914f8e4d..29040583aa2 100644 --- a/db/migrate/20161117114805_remove_undeleted_groups.rb +++ b/db/migrate/20161117114805_remove_undeleted_groups.rb @@ -5,47 +5,87 @@ class RemoveUndeletedGroups < ActiveRecord::Migration DOWNTIME = false def up + is_ee = defined?(Gitlab::License) + + if is_ee + execute <<-EOF.strip_heredoc + DELETE FROM path_locks + WHERE project_id IN ( + SELECT project_id + FROM projects + WHERE namespace_id IN (#{namespaces_pending_removal}) + ); + EOF + + execute <<-EOF.strip_heredoc + DELETE FROM remote_mirrors + WHERE project_id IN ( + SELECT project_id + FROM projects + WHERE namespace_id IN (#{namespaces_pending_removal}) + ); + EOF + end + execute <<-EOF.strip_heredoc - DELETE FROM projects - WHERE namespace_id IN ( - SELECT id FROM ( - SELECT id - FROM namespaces - WHERE deleted_at IS NOT NULL - ) namespace_ids + DELETE FROM lists + WHERE label_id IN ( + SELECT id + FROM labels + WHERE group_id IN (#{namespaces_pending_removal}) + ); + EOF + + execute <<-EOF.strip_heredoc + DELETE FROM lists + WHERE board_id IN ( + SELECT id + FROM boards + WHERE project_id IN ( + SELECT project_id + FROM projects + WHERE namespace_id IN (#{namespaces_pending_removal}) + ) ); EOF - if defined?(Gitlab::License) + execute <<-EOF.strip_heredoc + DELETE FROM labels + WHERE group_id IN (#{namespaces_pending_removal}); + EOF + + execute <<-EOF.strip_heredoc + DELETE FROM boards + WHERE project_id IN ( + SELECT project_id + FROM projects + WHERE namespace_id IN (#{namespaces_pending_removal}) + ) + EOF + + execute <<-EOF.strip_heredoc + DELETE FROM projects + WHERE namespace_id IN (#{namespaces_pending_removal}); + EOF + + if is_ee # EE adds these columns but we have to make sure this data is cleaned up # here before we run the DELETE below. An alternative would be patching # this migration in EE but this will only result in a mess and confusing # migrations. execute <<-EOF.strip_heredoc DELETE FROM protected_branch_push_access_levels - WHERE group_id IN ( - SELECT id FROM ( - SELECT id - FROM namespaces - WHERE deleted_at IS NOT NULL - ) namespace_ids - ); + WHERE group_id IN (#{namespaces_pending_removal}); EOF execute <<-EOF.strip_heredoc DELETE FROM protected_branch_merge_access_levels - WHERE group_id IN ( - SELECT id FROM ( - SELECT id - FROM namespaces - WHERE deleted_at IS NOT NULL - ) namespace_ids - ); + WHERE group_id IN (#{namespaces_pending_removal}); EOF end - # This removes namespaces that were supposed to be soft deleted but still - # reside in the database. + # This removes namespaces that were supposed to be deleted but still reside + # in the database. execute "DELETE FROM namespaces WHERE deleted_at IS NOT NULL;" end @@ -54,4 +94,12 @@ class RemoveUndeletedGroups < ActiveRecord::Migration # If someone is trying to rollback for other reasons, we should not throw an Exception. # raise ActiveRecord::IrreversibleMigration end + + def namespaces_pending_removal + "SELECT id FROM ( + SELECT id + FROM namespaces + WHERE deleted_at IS NOT NULL + ) namespace_ids" + end end -- cgit v1.2.1 From c3a2d57415ec32d257c557bd24df8e6aab7fadae Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Fri, 6 Jan 2017 15:46:49 +0100 Subject: Fill missing authorized projects rows This ensures that the project_authorizations rows exist for all users for which this data has not yet been populated. Fixes #26194 --- changelogs/unreleased/fill-authorized-projects.yml | 4 +++ .../20170106142508_fill_authorized_projects.rb | 30 ++++++++++++++++++++++ spec/migrations/fill_authorized_projects_spec.rb | 18 +++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 changelogs/unreleased/fill-authorized-projects.yml create mode 100644 db/post_migrate/20170106142508_fill_authorized_projects.rb create mode 100644 spec/migrations/fill_authorized_projects_spec.rb diff --git a/changelogs/unreleased/fill-authorized-projects.yml b/changelogs/unreleased/fill-authorized-projects.yml new file mode 100644 index 00000000000..e8e33011a15 --- /dev/null +++ b/changelogs/unreleased/fill-authorized-projects.yml @@ -0,0 +1,4 @@ +--- +title: Fill missing authorized projects rows +merge_request: +author: diff --git a/db/post_migrate/20170106142508_fill_authorized_projects.rb b/db/post_migrate/20170106142508_fill_authorized_projects.rb new file mode 100644 index 00000000000..314c8440c8b --- /dev/null +++ b/db/post_migrate/20170106142508_fill_authorized_projects.rb @@ -0,0 +1,30 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class FillAuthorizedProjects < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + class User < ActiveRecord::Base + self.table_name = 'users' + end + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + # We're not inserting any data so we don't need to start a transaction. + disable_ddl_transaction! + + def up + relation = User.select(:id). + where('authorized_projects_populated IS NOT TRUE') + + relation.find_in_batches(batch_size: 1_000) do |rows| + args = rows.map { |row| [row.id] } + + Sidekiq::Client.push_bulk('class' => 'AuthorizedProjectsWorker', 'args' => args) + end + end + + def down + end +end diff --git a/spec/migrations/fill_authorized_projects_spec.rb b/spec/migrations/fill_authorized_projects_spec.rb new file mode 100644 index 00000000000..99dc4195818 --- /dev/null +++ b/spec/migrations/fill_authorized_projects_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20170106142508_fill_authorized_projects.rb') + +describe FillAuthorizedProjects do + describe '#up' do + it 'schedules the jobs in batches' do + user1 = create(:user) + user2 = create(:user) + + expect(Sidekiq::Client).to receive(:push_bulk).with( + 'class' => 'AuthorizedProjectsWorker', + 'args' => [[user1.id], [user2.id]] + ) + + described_class.new.up + end + end +end -- cgit v1.2.1 From dafd5531ed793321eb8d886715654b4d572b2880 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 10 Jan 2017 14:40:08 +0100 Subject: Refactor authorized params in Admin::ApplicationSettingsController MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- .../admin/application_settings_controller.rb | 115 +++++++++++++-------- 1 file changed, 70 insertions(+), 45 deletions(-) diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index c2bb8464824..8dbdeca4abd 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -67,69 +67,94 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController params.delete(:domain_blacklist_raw) if params[:domain_blacklist_file] params.require(:application_setting).permit( - :default_projects_limit, - :default_branch_protection, - :signup_enabled, - :signin_enabled, - :require_two_factor_authentication, - :two_factor_grace_period, - :gravatar_enabled, - :sign_in_text, - :after_sign_up_text, - :help_page_text, - :home_page_url, + application_setting_params_ce + ) + end + + def application_setting_params_ce + [ + :admin_notification_email, :after_sign_out_path, - :max_attachment_size, - :session_expire_delay, + :after_sign_up_text, + + :akismet_api_key, + :akismet_enabled, + + :container_registry_token_expire_delay, + + :default_branch_protection, + :default_group_visibility, :default_project_visibility, + :default_projects_limit, :default_snippet_visibility, - :default_group_visibility, - :domain_whitelist_raw, + :domain_blacklist_enabled, - :domain_blacklist_raw, :domain_blacklist_file, - :version_check_enabled, - :admin_notification_email, - :user_oauth_applications, - :user_default_external, - :shared_runners_enabled, - :shared_runners_text, + :domain_blacklist_raw, + :domain_whitelist_raw, + + :email_author_in_body, + :enabled_git_access_protocol, + :gravatar_enabled, + :help_page_text, + :home_page_url, + + :housekeeping_bitmaps_enabled, + :housekeeping_enabled, + :housekeeping_full_repack_period, + :housekeeping_gc_period, + :housekeeping_incremental_repack_period, + + :html_emails_enabled, + + :koding_enabled, + :koding_url, + :max_artifacts_size, + :max_attachment_size, + :metrics_enabled, :metrics_host, - :metrics_port, - :metrics_pool_size, - :metrics_timeout, :metrics_method_call_threshold, + :metrics_packet_size, + :metrics_pool_size, + :metrics_port, :metrics_sample_interval, + :metrics_timeout, + :recaptcha_enabled, - :recaptcha_site_key, :recaptcha_private_key, - :sentry_enabled, - :sentry_dsn, - :akismet_enabled, - :akismet_api_key, - :koding_enabled, - :koding_url, - :email_author_in_body, - :html_emails_enabled, + :recaptcha_site_key, + :repository_checks_enabled, - :metrics_packet_size, + :require_two_factor_authentication, + :session_expire_delay, + + :sign_in_text, + :signin_enabled, + :signup_enabled, + + :sentry_dsn, + :sentry_enabled, + :send_user_confirmation_email, - :container_registry_token_expire_delay, - :enabled_git_access_protocol, + + :shared_runners_enabled, + :shared_runners_text, + :sidekiq_throttling_enabled, :sidekiq_throttling_factor, - :housekeeping_enabled, - :housekeeping_bitmaps_enabled, - :housekeeping_incremental_repack_period, - :housekeeping_full_repack_period, - :housekeeping_gc_period, + + :two_factor_grace_period, + :user_default_external, + :user_oauth_applications, + :version_check_enabled, + + disabled_oauth_sign_in_sources: [], + import_sources: [], repository_storages: [], restricted_visibility_levels: [], - import_sources: [], - disabled_oauth_sign_in_sources: [], sidekiq_throttling_queues: [] - ) + ] end end -- cgit v1.2.1 From 83c1395af2e457a1de919a67ff7a2b21dceaeeef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 10 Jan 2017 14:43:06 +0100 Subject: Refactor authorized params in Admin::GroupsController MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- app/controllers/admin/groups_controller.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index add1c819adf..b7722a1d15d 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -61,7 +61,11 @@ class Admin::GroupsController < Admin::ApplicationController end def group_params - params.require(:group).permit( + params.require(:group).permit(group_params_ce) + end + + def group_params_ce + [ :avatar, :description, :lfs_enabled, @@ -69,6 +73,6 @@ class Admin::GroupsController < Admin::ApplicationController :path, :request_access_enabled, :visibility_level - ) + ] end end -- cgit v1.2.1 From d8d9d0c74cdc0ec68bce23ba75706c1b78e864d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 10 Jan 2017 14:53:20 +0100 Subject: Refactor authorized params in Admin::UsersController MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- .../admin/application_settings_controller.rb | 18 ---------- app/controllers/admin/users_controller.rb | 41 +++++++++++++++++----- 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 8dbdeca4abd..e34ba424497 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -76,43 +76,33 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :admin_notification_email, :after_sign_out_path, :after_sign_up_text, - :akismet_api_key, :akismet_enabled, - :container_registry_token_expire_delay, - :default_branch_protection, :default_group_visibility, :default_project_visibility, :default_projects_limit, :default_snippet_visibility, - :domain_blacklist_enabled, :domain_blacklist_file, :domain_blacklist_raw, :domain_whitelist_raw, - :email_author_in_body, :enabled_git_access_protocol, :gravatar_enabled, :help_page_text, :home_page_url, - :housekeeping_bitmaps_enabled, :housekeeping_enabled, :housekeeping_full_repack_period, :housekeeping_gc_period, :housekeeping_incremental_repack_period, - :html_emails_enabled, - :koding_enabled, :koding_url, - :max_artifacts_size, :max_attachment_size, - :metrics_enabled, :metrics_host, :metrics_method_call_threshold, @@ -121,30 +111,22 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :metrics_port, :metrics_sample_interval, :metrics_timeout, - :recaptcha_enabled, :recaptcha_private_key, :recaptcha_site_key, - :repository_checks_enabled, :require_two_factor_authentication, :session_expire_delay, - :sign_in_text, :signin_enabled, :signup_enabled, - :sentry_dsn, :sentry_enabled, - :send_user_confirmation_email, - :shared_runners_enabled, :shared_runners_text, - :sidekiq_throttling_enabled, :sidekiq_throttling_factor, - :two_factor_grace_period, :user_default_external, :user_oauth_applications, diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index df9039b16b2..aa0f8d434dc 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -161,15 +161,6 @@ class Admin::UsersController < Admin::ApplicationController @user ||= User.find_by!(username: params[:id]) end - def user_params - params.require(:user).permit( - :email, :remember_me, :bio, :name, :username, - :skype, :linkedin, :twitter, :website_url, :color_scheme_id, :theme_id, :force_random_password, - :extern_uid, :provider, :password_expires_at, :avatar, :hide_no_ssh_key, :hide_no_password, - :projects_limit, :can_create_group, :admin, :key_id, :external - ) - end - def redirect_back_or_admin_user(options = {}) redirect_back_or_default(default: default_route, options: options) end @@ -177,4 +168,36 @@ class Admin::UsersController < Admin::ApplicationController def default_route [:admin, @user] end + + def user_params + params.require(:user).permit(user_params_ce) + end + + def user_params_ce + [ + :admin, + :avatar, + :bio, + :can_create_group, + :color_scheme_id, + :email, + :extern_uid, + :external, + :force_random_password, + :hide_no_password, + :hide_no_ssh_key, + :key_id, + :linkedin, + :name, + :password_expires_at, + :projects_limit, + :provider, + :remember_me, + :skype, + :theme_id, + :twitter, + :username, + :website_url + ] + end end -- cgit v1.2.1 From ce9d3ee599f816ba20d57eae16b4139cabecdb1a Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 11 Jan 2017 20:47:42 -0500 Subject: Backend review --- .../javascripts/droplab/droplab_ajax_filter.js | 9 +- .../filtered_search/dropdown_assignee_spec.rb | 18 ++++ .../issues/filtered_search/dropdown_author_spec.rb | 16 ++++ .../issues/filtered_search/dropdown_hint_spec.rb | 11 +++ .../issues/filtered_search/dropdown_label_spec.rb | 25 +++++ .../filtered_search/dropdown_milestone_spec.rb | 25 +++++ .../issues/filtered_search/filter_issues_spec.rb | 103 ++++++++++++++++++--- .../issues/filtered_search/search_bar_spec.rb | 7 ++ 8 files changed, 195 insertions(+), 19 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab_ajax_filter.js b/app/assets/javascripts/droplab/droplab_ajax_filter.js index ae316f881c8..af163f76851 100644 --- a/app/assets/javascripts/droplab/droplab_ajax_filter.js +++ b/app/assets/javascripts/droplab/droplab_ajax_filter.js @@ -22,7 +22,8 @@ require('../window')(function(w){ debounceTrigger: function debounceTrigger(e) { var NON_CHARACTER_KEYS = [16, 17, 18, 20, 37, 38, 39, 40, 91, 93]; var invalidKeyPressed = NON_CHARACTER_KEYS.indexOf(e.detail.which || e.detail.keyCode) > -1; - var focusEvent = false; + var focusEvent = e.type === 'focus'; + if (invalidKeyPressed || this.loading) { return; } @@ -31,10 +32,6 @@ require('../window')(function(w){ clearTimeout(this.timeout); } - if (e.type === 'focus') { - focusEvent = true; - } - this.timeout = setTimeout(this.trigger.bind(this, focusEvent), 200); }, @@ -66,7 +63,7 @@ require('../window')(function(w){ searchValue = ''; } - if (searchValue === config.searchKey) { + if (config.searchKey === searchValue) { return this.list.show(); } diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb index a5a05a622b6..6f6a2532c04 100644 --- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb @@ -39,26 +39,31 @@ describe 'Dropdown assignee', js: true, feature: true do describe 'behavior' do it 'opens when the search bar has assignee:' do filtered_search.set('assignee:') + expect(page).to have_css(js_dropdown_assignee, visible: true) end it 'closes when the search bar is unfocused' do find('body').click() + expect(page).to have_css(js_dropdown_assignee, visible: false) end it 'should show loading indicator when opened' do filtered_search.set('assignee:') + expect(page).to have_css('#js-dropdown-assignee .filter-dropdown-loading', visible: true) end it 'should hide loading indicator when loaded' do send_keys_to_filtered_search('assignee:') + expect(page).not_to have_css('#js-dropdown-assignee .filter-dropdown-loading') end it 'should load all the assignees when opened' do send_keys_to_filtered_search('assignee:') + expect(dropdown_assignee_size).to eq(3) end end @@ -70,31 +75,37 @@ describe 'Dropdown assignee', js: true, feature: true do it 'filters by name' do send_keys_to_filtered_search('j') + expect(dropdown_assignee_size).to eq(2) end it 'filters by case insensitive name' do send_keys_to_filtered_search('J') + expect(dropdown_assignee_size).to eq(2) end it 'filters by username with symbol' do send_keys_to_filtered_search('@ot') + expect(dropdown_assignee_size).to eq(2) end it 'filters by case insensitive username with symbol' do send_keys_to_filtered_search('@OT') + expect(dropdown_assignee_size).to eq(2) end it 'filters by username without symbol' do send_keys_to_filtered_search('ot') + expect(dropdown_assignee_size).to eq(2) end it 'filters by case insensitive username without symbol' do send_keys_to_filtered_search('OT') + expect(dropdown_assignee_size).to eq(2) end end @@ -106,6 +117,7 @@ describe 'Dropdown assignee', js: true, feature: true do it 'fills in the assignee username when the assignee has not been filtered' do click_assignee(user_jacob.name) + expect(page).to have_css(js_dropdown_assignee, visible: false) expect(filtered_search.value).to eq("assignee:@#{user_jacob.username}") end @@ -113,12 +125,14 @@ describe 'Dropdown assignee', js: true, feature: true do it 'fills in the assignee username when the assignee has been filtered' do send_keys_to_filtered_search('roo') click_assignee(user.name) + expect(page).to have_css(js_dropdown_assignee, visible: false) expect(filtered_search.value).to eq("assignee:@#{user.username}") end it 'selects `no assignee`' do find('#js-dropdown-assignee .filter-dropdown-item', text: 'No Assignee').click + expect(page).to have_css(js_dropdown_assignee, visible: false) expect(filtered_search.value).to eq("assignee:none") end @@ -127,21 +141,25 @@ describe 'Dropdown assignee', js: true, feature: true do describe 'input has existing content' do it 'opens assignee dropdown with existing search term' do filtered_search.set('searchTerm assignee:') + expect(page).to have_css(js_dropdown_assignee, visible: true) end it 'opens assignee dropdown with existing author' do filtered_search.set('author:@user assignee:') + expect(page).to have_css(js_dropdown_assignee, visible: true) end it 'opens assignee dropdown with existing label' do filtered_search.set('label:~bug assignee:') + expect(page).to have_css(js_dropdown_assignee, visible: true) end it 'opens assignee dropdown with existing milestone' do filtered_search.set('milestone:%v1.0 assignee:') + expect(page).to have_css(js_dropdown_assignee, visible: true) end end diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb index d053b032768..60a86cc93d4 100644 --- a/spec/features/issues/filtered_search/dropdown_author_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb @@ -39,26 +39,31 @@ describe 'Dropdown author', js: true, feature: true do describe 'behavior' do it 'opens when the search bar has author:' do filtered_search.set('author:') + expect(page).to have_css(js_dropdown_author, visible: true) end it 'closes when the search bar is unfocused' do find('body').click() + expect(page).to have_css(js_dropdown_author, visible: false) end it 'should show loading indicator when opened' do filtered_search.set('author:') + expect(page).to have_css('#js-dropdown-author .filter-dropdown-loading', visible: true) end it 'should hide loading indicator when loaded' do send_keys_to_filtered_search('author:') + expect(page).not_to have_css('#js-dropdown-author .filter-dropdown-loading') end it 'should load all the authors when opened' do send_keys_to_filtered_search('author:') + expect(dropdown_author_size).to eq(3) end end @@ -71,26 +76,31 @@ describe 'Dropdown author', js: true, feature: true do it 'filters by name' do send_keys_to_filtered_search('ja') + expect(dropdown_author_size).to eq(1) end it 'filters by case insensitive name' do send_keys_to_filtered_search('Ja') + expect(dropdown_author_size).to eq(1) end it 'filters by username with symbol' do send_keys_to_filtered_search('@ot') + expect(dropdown_author_size).to eq(2) end it 'filters by username without symbol' do send_keys_to_filtered_search('ot') + expect(dropdown_author_size).to eq(2) end it 'filters by case insensitive username without symbol' do send_keys_to_filtered_search('OT') + expect(dropdown_author_size).to eq(2) end end @@ -103,12 +113,14 @@ describe 'Dropdown author', js: true, feature: true do it 'fills in the author username when the author has not been filtered' do click_author(user_jacob.name) + expect(page).to have_css(js_dropdown_author, visible: false) expect(filtered_search.value).to eq("author:@#{user_jacob.username}") end it 'fills in the author username when the author has been filtered' do click_author(user.name) + expect(page).to have_css(js_dropdown_author, visible: false) expect(filtered_search.value).to eq("author:@#{user.username}") end @@ -117,21 +129,25 @@ describe 'Dropdown author', js: true, feature: true do describe 'input has existing content' do it 'opens author dropdown with existing search term' do filtered_search.set('searchTerm author:') + expect(page).to have_css(js_dropdown_author, visible: true) end it 'opens author dropdown with existing assignee' do filtered_search.set('assignee:@user author:') + expect(page).to have_css(js_dropdown_author, visible: true) end it 'opens author dropdown with existing label' do filtered_search.set('label:~bug author:') + expect(page).to have_css(js_dropdown_author, visible: true) end it 'opens author dropdown with existing milestone' do filtered_search.set('milestone:%v1.0 author:') + expect(page).to have_css(js_dropdown_author, visible: true) end end diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb index 910abb56569..04dd54ab459 100644 --- a/spec/features/issues/filtered_search/dropdown_hint_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb @@ -36,6 +36,7 @@ describe 'Dropdown hint', js: true, feature: true do it 'closes when the search bar is unfocused' do find('body').click + expect(page).to have_css(js_dropdown_hint, visible: false) end end @@ -43,12 +44,14 @@ describe 'Dropdown hint', js: true, feature: true do describe 'filtering' do it 'does not filter `Keep typing and press Enter`' do filtered_search.set('randomtext') + expect(page).to have_css(js_dropdown_hint, text: 'Keep typing and press Enter', visible: false) expect(dropdown_hint_size).to eq(0) end it 'filters with text' do filtered_search.set('a') + expect(dropdown_hint_size).to eq(3) end end @@ -60,6 +63,7 @@ describe 'Dropdown hint', js: true, feature: true do it 'opens the author dropdown when you click on author' do click_hint('author') + expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-author', visible: true) expect(filtered_search.value).to eq('author:') @@ -67,6 +71,7 @@ describe 'Dropdown hint', js: true, feature: true do it 'opens the assignee dropdown when you click on assignee' do click_hint('assignee') + expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-assignee', visible: true) expect(filtered_search.value).to eq('assignee:') @@ -74,6 +79,7 @@ describe 'Dropdown hint', js: true, feature: true do it 'opens the milestone dropdown when you click on milestone' do click_hint('milestone') + expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-milestone', visible: true) expect(filtered_search.value).to eq('milestone:') @@ -81,6 +87,7 @@ describe 'Dropdown hint', js: true, feature: true do it 'opens the label dropdown when you click on label' do click_hint('label') + expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-label', visible: true) expect(filtered_search.value).to eq('label:') @@ -91,6 +98,7 @@ describe 'Dropdown hint', js: true, feature: true do it 'opens the author dropdown when you click on author' do filtered_search.set('auth') click_hint('author') + expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-author', visible: true) expect(filtered_search.value).to eq('author:') @@ -99,6 +107,7 @@ describe 'Dropdown hint', js: true, feature: true do it 'opens the assignee dropdown when you click on assignee' do filtered_search.set('assign') click_hint('assignee') + expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-assignee', visible: true) expect(filtered_search.value).to eq('assignee:') @@ -107,6 +116,7 @@ describe 'Dropdown hint', js: true, feature: true do it 'opens the milestone dropdown when you click on milestone' do filtered_search.set('mile') click_hint('milestone') + expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-milestone', visible: true) expect(filtered_search.value).to eq('milestone:') @@ -115,6 +125,7 @@ describe 'Dropdown hint', js: true, feature: true do it 'opens the label dropdown when you click on label' do filtered_search.set('lab') click_hint('label') + expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-label', visible: true) expect(filtered_search.value).to eq('label:') diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb index 1a21b0d64ed..89c144141c9 100644 --- a/spec/features/issues/filtered_search/dropdown_label_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb @@ -43,26 +43,31 @@ describe 'Dropdown label', js: true, feature: true do describe 'behavior' do it 'opens when the search bar has label:' do filtered_search.set('label:') + expect(page).to have_css(js_dropdown_label, visible: true) end it 'closes when the search bar is unfocused' do find('body').click() + expect(page).to have_css(js_dropdown_label, visible: false) end it 'should show loading indicator when opened' do filtered_search.set('label:') + expect(page).to have_css('#js-dropdown-label .filter-dropdown-loading', visible: true) end it 'should hide loading indicator when loaded' do send_keys_to_filtered_search('label:') + expect(page).not_to have_css('#js-dropdown-label .filter-dropdown-loading') end it 'should load all the labels when opened' do send_keys_to_filtered_search('label:') + expect(dropdown_label_size).to be > 0 end end @@ -74,61 +79,73 @@ describe 'Dropdown label', js: true, feature: true do it 'filters by name' do send_keys_to_filtered_search(':b') + expect(dropdown_label_size).to eq(2) end it 'filters by case insensitive name' do send_keys_to_filtered_search(':B') + expect(dropdown_label_size).to eq(2) end it 'filters by name with symbol' do send_keys_to_filtered_search(':~bu') + expect(dropdown_label_size).to eq(2) end it 'filters by case insensitive name with symbol' do send_keys_to_filtered_search(':~BU') + expect(dropdown_label_size).to eq(2) end it 'filters by multiple words' do send_keys_to_filtered_search(':Hig') + expect(dropdown_label_size).to eq(1) end it 'filters by multiple words with symbol' do send_keys_to_filtered_search(':~Hig') + expect(dropdown_label_size).to eq(1) end it 'filters by multiple words containing single quotes' do send_keys_to_filtered_search(':won\'t') + expect(dropdown_label_size).to eq(1) end it 'filters by multiple words containing single quotes with symbol' do send_keys_to_filtered_search(':~won\'t') + expect(dropdown_label_size).to eq(1) end it 'filters by multiple words containing double quotes' do send_keys_to_filtered_search(':won"t') + expect(dropdown_label_size).to eq(1) end it 'filters by multiple words containing double quotes with symbol' do send_keys_to_filtered_search(':~won"t') + expect(dropdown_label_size).to eq(1) end it 'filters by special characters' do send_keys_to_filtered_search(':^+') + expect(dropdown_label_size).to eq(1) end it 'filters by special characters with symbol' do send_keys_to_filtered_search(':~^+') + expect(dropdown_label_size).to eq(1) end end @@ -140,6 +157,7 @@ describe 'Dropdown label', js: true, feature: true do it 'fills in the label name when the label has not been filled' do click_label(bug_label.title) + expect(page).to have_css(js_dropdown_label, visible: false) expect(filtered_search.value).to eq("label:~#{bug_label.title}") end @@ -147,42 +165,49 @@ describe 'Dropdown label', js: true, feature: true do it 'fills in the label name when the label is partially filled' do send_keys_to_filtered_search('bu') click_label(bug_label.title) + expect(page).to have_css(js_dropdown_label, visible: false) expect(filtered_search.value).to eq("label:~#{bug_label.title}") end it 'fills in the label name that contains multiple words' do click_label(two_words_label.title) + expect(page).to have_css(js_dropdown_label, visible: false) expect(filtered_search.value).to eq("label:~\"#{two_words_label.title}\"") end it 'fills in the label name that contains multiple words and is very long' do click_label(long_label.title) + expect(page).to have_css(js_dropdown_label, visible: false) expect(filtered_search.value).to eq("label:~\"#{long_label.title}\"") end it 'fills in the label name that contains double quotes' do click_label(wont_fix_label.title) + expect(page).to have_css(js_dropdown_label, visible: false) expect(filtered_search.value).to eq("label:~'#{wont_fix_label.title}'") end it 'fills in the label name with the correct capitalization' do click_label(uppercase_label.title) + expect(page).to have_css(js_dropdown_label, visible: false) expect(filtered_search.value).to eq("label:~#{uppercase_label.title}") end it 'fills in the label name with special characters' do click_label(special_label.title) + expect(page).to have_css(js_dropdown_label, visible: false) expect(filtered_search.value).to eq("label:~#{special_label.title}") end it 'selects `no label`' do find('#js-dropdown-label .filter-dropdown-item', text: 'No Label').click + expect(page).to have_css(js_dropdown_label, visible: false) expect(filtered_search.value).to eq("label:none") end diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb index 64fc83c6ccb..e5a271b663f 100644 --- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb @@ -47,26 +47,31 @@ describe 'Dropdown milestone', js: true, feature: true do describe 'behavior' do it 'opens when the search bar has milestone:' do filtered_search.set('milestone:') + expect(page).to have_css(js_dropdown_milestone, visible: true) end it 'closes when the search bar is unfocused' do find('body').click() + expect(page).to have_css(js_dropdown_milestone, visible: false) end it 'should show loading indicator when opened' do filtered_search.set('milestone:') + expect(page).to have_css('#js-dropdown-milestone .filter-dropdown-loading', visible: true) end it 'should hide loading indicator when loaded' do send_keys_to_filtered_search('milestone:') + expect(page).not_to have_css('#js-dropdown-milestone .filter-dropdown-loading') end it 'should load all the milestones when opened' do send_keys_to_filtered_search('milestone:') + expect(dropdown_milestone_size).to be > 0 end end @@ -78,31 +83,37 @@ describe 'Dropdown milestone', js: true, feature: true do it 'filters by name' do send_keys_to_filtered_search(':v1') + expect(dropdown_milestone_size).to eq(1) end it 'filters by case insensitive name' do send_keys_to_filtered_search(':V1') + expect(dropdown_milestone_size).to eq(1) end it 'filters by name with symbol' do send_keys_to_filtered_search(':%v1') + expect(dropdown_milestone_size).to eq(1) end it 'filters by case insensitive name with symbol' do send_keys_to_filtered_search(':%V1') + expect(dropdown_milestone_size).to eq(1) end it 'filters by special characters' do send_keys_to_filtered_search(':(+') + expect(dropdown_milestone_size).to eq(1) end it 'filters by special characters with symbol' do send_keys_to_filtered_search(':%(+') + expect(dropdown_milestone_size).to eq(1) end end @@ -114,6 +125,7 @@ describe 'Dropdown milestone', js: true, feature: true do it 'fills in the milestone name when the milestone has not been filled' do click_milestone(milestone.title) + expect(page).to have_css(js_dropdown_milestone, visible: false) expect(filtered_search.value).to eq("milestone:%#{milestone.title}") end @@ -121,48 +133,56 @@ describe 'Dropdown milestone', js: true, feature: true do it 'fills in the milestone name when the milestone is partially filled' do send_keys_to_filtered_search('v') click_milestone(milestone.title) + expect(page).to have_css(js_dropdown_milestone, visible: false) expect(filtered_search.value).to eq("milestone:%#{milestone.title}") end it 'fills in the milestone name that contains multiple words' do click_milestone(two_words_milestone.title) + expect(page).to have_css(js_dropdown_milestone, visible: false) expect(filtered_search.value).to eq("milestone:%\"#{two_words_milestone.title}\"") end it 'fills in the milestone name that contains multiple words and is very long' do click_milestone(long_milestone.title) + expect(page).to have_css(js_dropdown_milestone, visible: false) expect(filtered_search.value).to eq("milestone:%\"#{long_milestone.title}\"") end it 'fills in the milestone name that contains double quotes' do click_milestone(wont_fix_milestone.title) + expect(page).to have_css(js_dropdown_milestone, visible: false) expect(filtered_search.value).to eq("milestone:%'#{wont_fix_milestone.title}'") end it 'fills in the milestone name with the correct capitalization' do click_milestone(uppercase_milestone.title) + expect(page).to have_css(js_dropdown_milestone, visible: false) expect(filtered_search.value).to eq("milestone:%#{uppercase_milestone.title}") end it 'fills in the milestone name with special characters' do click_milestone(special_milestone.title) + expect(page).to have_css(js_dropdown_milestone, visible: false) expect(filtered_search.value).to eq("milestone:%#{special_milestone.title}") end it 'selects `no milestone`' do click_static_milestone('No Milestone') + expect(page).to have_css(js_dropdown_milestone, visible: false) expect(filtered_search.value).to eq("milestone:none") end it 'selects `upcoming milestone`' do click_static_milestone('Upcoming') + expect(page).to have_css(js_dropdown_milestone, visible: false) expect(filtered_search.value).to eq("milestone:upcoming") end @@ -171,26 +191,31 @@ describe 'Dropdown milestone', js: true, feature: true do describe 'input has existing content' do it 'opens milestone dropdown with existing search term' do filtered_search.set('searchTerm milestone:') + expect(page).to have_css(js_dropdown_milestone, visible: true) end it 'opens milestone dropdown with existing author' do filtered_search.set('author:@john milestone:') + expect(page).to have_css(js_dropdown_milestone, visible: true) end it 'opens milestone dropdown with existing assignee' do filtered_search.set('assignee:@john milestone:') + expect(page).to have_css(js_dropdown_milestone, visible: true) end it 'opens milestone dropdown with existing label' do filtered_search.set('label:~important milestone:') + expect(page).to have_css(js_dropdown_milestone, visible: true) end it 'opens milestone dropdown with existing milestone' do filtered_search.set('milestone:%100 milestone:') + expect(page).to have_css(js_dropdown_milestone, visible: true) end end diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index ed6d4a0787a..7cae7152aa2 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -101,15 +101,18 @@ describe 'Filter issues', js: true, feature: true do context 'only author' do it 'filters issues by searched author' do input_filtered_search("author:@#{user.username}") + expect_issues_list_count(5) end it 'filters issues by invalid author' do - # YOLO + pending('to be tested, issue #26546') + fail end it 'filters issues by multiple authors' do - # YOLO + pending('to be tested, issue #26546') + fail end end @@ -117,6 +120,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched author and text' do search = "author:@#{user.username} issue" input_filtered_search(search) + expect_issues_list_count(3) expect_filtered_search_input(search) end @@ -124,6 +128,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched author, assignee and text' do search = "author:@#{user.username} assignee:@#{user.username} issue" input_filtered_search(search) + expect_issues_list_count(3) expect_filtered_search_input(search) end @@ -131,6 +136,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched author, assignee, label, and text' do search = "author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} issue" input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -138,13 +144,15 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched author, assignee, label, milestone and text' do search = "author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} issue" input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end end - context 'sorting' do - # TODO + it 'sorting' do + pending('to be tested, issue #26546') + fail end end @@ -153,6 +161,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched assignee' do search = "assignee:@#{user.username}" input_filtered_search(search) + expect_issues_list_count(5) expect_filtered_search_input(search) end @@ -160,16 +169,19 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by no assignee' do search = "assignee:none" input_filtered_search(search) + expect_issues_list_count(8, 1) expect_filtered_search_input(search) end it 'filters issues by invalid assignee' do - # YOLO + pending('to be tested, issue #26546') + fail end it 'filters issues by multiple assignees' do - # YOLO + pending('to be tested, issue #26546') + fail end end @@ -177,6 +189,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched assignee and text' do search = "assignee:@#{user.username} searchTerm" input_filtered_search(search) + expect_issues_list_count(2) expect_filtered_search_input(search) end @@ -184,6 +197,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched assignee, author and text' do search = "assignee:@#{user.username} author:@#{user.username} searchTerm" input_filtered_search(search) + expect_issues_list_count(2) expect_filtered_search_input(search) end @@ -191,6 +205,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched assignee, author, label, text' do search = "assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} searchTerm" input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -198,13 +213,17 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched assignee, author, label, milestone and text' do search = "assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} searchTerm" input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end end context 'sorting' do - # TODO + it 'sorts' do + pending('to be tested, issue #26546') + fail + end end end @@ -213,6 +232,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched label' do search = "label:~#{bug_label.title}" input_filtered_search(search) + expect_issues_list_count(2) expect_filtered_search_input(search) end @@ -220,17 +240,20 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by no label' do search = "label:none" input_filtered_search(search) + expect_issues_list_count(9, 1) expect_filtered_search_input(search) end it 'filters issues by invalid label' do - # YOLO + pending('to be tested, issue #26546') + fail end it 'filters issues by multiple labels' do search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title}" input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -242,6 +265,7 @@ describe 'Filter issues', js: true, feature: true do search = "label:~#{special_label.title}" input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -251,6 +275,7 @@ describe 'Filter issues', js: true, feature: true do search = "label:~#{new_label.title}" input_filtered_search(search) + expect_no_issues_list() expect_filtered_search_input(search) end @@ -264,6 +289,7 @@ describe 'Filter issues', js: true, feature: true do search = "label:~'#{special_multiple_label.title}'" input_filtered_search(search) + expect_issues_list_count(1) # filtered search defaults quotations to double quotes @@ -273,14 +299,15 @@ describe 'Filter issues', js: true, feature: true do it 'single quotes' do search = "label:~'#{multiple_words_label.title}'" input_filtered_search(search) - expect_issues_list_count(1) + expect_issues_list_count(1) expect_filtered_search_input("label:~\"#{multiple_words_label.title}\"") end it 'double quotes' do search = "label:~\"#{multiple_words_label.title}\"" input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -292,6 +319,7 @@ describe 'Filter issues', js: true, feature: true do search = "label:~'#{double_quotes_label.title}'" input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -303,6 +331,7 @@ describe 'Filter issues', js: true, feature: true do search = "label:~\"#{single_quotes_label.title}\"" input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -312,6 +341,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched label and text' do search = "label:~#{caps_sensitive_label.title} bug" input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -319,6 +349,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched label, author and text' do search = "label:~#{caps_sensitive_label.title} author:@#{user.username} bug" input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -326,6 +357,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched label, author, assignee and text' do search = "label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug" input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -333,6 +365,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched label, author, assignee, milestone and text' do search = "label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug" input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -342,6 +375,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched label, label2, and text' do search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} bug" input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -349,6 +383,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched label, label2, author and text' do search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} bug" input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -356,6 +391,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched label, label2, author, assignee and text' do search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug" input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -363,6 +399,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched label, label2, author, assignee, milestone and text' do search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug" input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -384,7 +421,10 @@ describe 'Filter issues', js: true, feature: true do end context 'sorting' do - # TODO + it 'sorts' do + pending('to be tested, issue #26546') + fail + end end end @@ -392,25 +432,30 @@ describe 'Filter issues', js: true, feature: true do context 'only milestone' do it 'filters issues by searched milestone' do input_filtered_search("milestone:%#{milestone.title}") + expect_issues_list_count(5) end it 'filters issues by no milestone' do input_filtered_search("milestone:none") + expect_issues_list_count(7, 1) end it 'filters issues by upcoming milestones' do input_filtered_search("milestone:upcoming") + expect_issues_list_count(1) end it 'filters issues by invalid milestones' do - # YOLO + pending('to be tested, issue #26546') + fail end it 'filters issues by multiple milestones' do - # YOLO + pending('to be tested, issue #26546') + fail end it 'filters issues by milestone containing special characters' do @@ -419,6 +464,7 @@ describe 'Filter issues', js: true, feature: true do search = "milestone:%#{special_milestone.title}" input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -428,6 +474,7 @@ describe 'Filter issues', js: true, feature: true do search = "milestone:%#{new_milestone.title}" input_filtered_search(search) + expect_no_issues_list() expect_filtered_search_input(search) end @@ -437,6 +484,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched milestone and text' do search = "milestone:%#{milestone.title} bug" input_filtered_search(search) + expect_issues_list_count(2) expect_filtered_search_input(search) end @@ -444,6 +492,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched milestone, author and text' do search = "milestone:%#{milestone.title} author:@#{user.username} bug" input_filtered_search(search) + expect_issues_list_count(2) expect_filtered_search_input(search) end @@ -451,6 +500,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched milestone, author, assignee and text' do search = "milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} bug" input_filtered_search(search) + expect_issues_list_count(2) expect_filtered_search_input(search) end @@ -458,13 +508,17 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched milestone, author, assignee, label and text' do search = "milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug" input_filtered_search(search) + expect_issues_list_count(2) expect_filtered_search_input(search) end end context 'sorting' do - # TODO + it 'sorts' do + pending('to be tested, issue #26546') + fail + end end end @@ -473,6 +527,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched text' do search = 'Bug' input_filtered_search(search) + expect_issues_list_count(4, 1) expect_filtered_search_input(search) end @@ -480,6 +535,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by multiple searched text' do search = 'Bug report' input_filtered_search(search) + expect_issues_list_count(3) expect_filtered_search_input(search) end @@ -487,6 +543,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by case insensitive searched text' do search = 'bug report' input_filtered_search(search) + expect_issues_list_count(3) expect_filtered_search_input(search) end @@ -494,6 +551,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched text containing single quotes' do search = '\'single quotes\'' input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -501,6 +559,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched text containing double quotes' do search = '"double quotes"' input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -508,6 +567,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched text containing special characters' do search = '!@#{$%^&*()-+' input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -515,6 +575,7 @@ describe 'Filter issues', js: true, feature: true do it 'does not show any issues' do search = 'testing' input_filtered_search(search) + expect_no_issues_list() expect_filtered_search_input(search) end @@ -523,66 +584,77 @@ describe 'Filter issues', js: true, feature: true do context 'searched text with other filters' do it 'filters issues by searched text and author' do input_filtered_search("bug author:@#{user.username}") + expect_issues_list_count(2) expect_filtered_search_input("author:@#{user.username} bug") end it 'filters issues by searched text, author and more text' do input_filtered_search("bug author:@#{user.username} report") + expect_issues_list_count(1) expect_filtered_search_input("author:@#{user.username} bug report") end it 'filters issues by searched text, author and assignee' do input_filtered_search("bug author:@#{user.username} assignee:@#{user.username}") + expect_issues_list_count(2) expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug") end it 'filters issues by searched text, author, more text and assignee' do input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username}") + expect_issues_list_count(1) expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug report") end it 'filters issues by searched text, author, more text, assignee and even more text' do input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with") + expect_issues_list_count(1) expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug report with") end it 'filters issues by searched text, author, assignee and label' do input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title}") + expect_issues_list_count(2) expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug") end it 'filters issues by searched text, author, text, assignee, text, label and text' do input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything") + expect_issues_list_count(1) expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug report with everything") end it 'filters issues by searched text, author, assignee, label and milestone' do input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title}") + expect_issues_list_count(2) expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title} bug") end it 'filters issues by searched text, author, text, assignee, text, label, text, milestone and text' do input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything milestone:%#{milestone.title} you") + expect_issues_list_count(1) expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title} bug report with everything you") end it 'filters issues by searched text, author, assignee, multiple labels and milestone' do input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title}") + expect_issues_list_count(1) expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} bug") end it 'filters issues by searched text, author, text, assignee, text, label1, text, label2, text, milestone and text' do input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything label:~#{caps_sensitive_label.title} you milestone:%#{milestone.title} thought") + expect_issues_list_count(1) expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} bug report with everything you thought") end @@ -605,6 +677,7 @@ describe 'Filter issues', js: true, feature: true do updated_at: 5.days.ago) input_filtered_search('days ago') + expect_issues_list_count(2) sort_toggle = find('.filtered-search-container .dropdown-toggle') @@ -639,6 +712,7 @@ describe 'Filter issues', js: true, feature: true do it 'closed state' do find('.issues-state-filters a', text: 'Closed').click wait_for_ajax + expect(page).to have_selector('.issues-list .issue', count: 1) expect(find('.issues-list .issue:first-of-type .issue-title-text a')).to have_content(closed_issue.title) end @@ -646,6 +720,7 @@ describe 'Filter issues', js: true, feature: true do it 'all state' do find('.issues-state-filters a', text: 'All').click wait_for_ajax + expect(page).to have_selector('.issues-list .issue', count: 5) end end @@ -657,6 +732,7 @@ describe 'Filter issues', js: true, feature: true do params = CGI.parse(URI.parse(link[:href]).query) auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query) + expect(params).to include('private_token' => [user.private_token]) expect(params).to include('milestone_title' => [milestone.title]) expect(params).to include('assignee_id' => [user.id.to_s]) @@ -671,6 +747,7 @@ describe 'Filter issues', js: true, feature: true do params = CGI.parse(URI.parse(link[:href]).query) auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query) + expect(params).to include('private_token' => [user.private_token]) expect(params).to include('milestone_title' => [milestone.title]) expect(params).to include('assignee_id' => [user.id.to_s]) diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb index d5db90ae506..56b1d354eb0 100644 --- a/spec/features/issues/filtered_search/search_bar_spec.rb +++ b/spec/features/issues/filtered_search/search_bar_spec.rb @@ -27,6 +27,7 @@ describe 'Search bar', js: true, feature: true do expect(filtered_search.value).to eq(search_text) find('.filtered-search-input-container .clear-search').click + expect(filtered_search.value).to eq('') end @@ -37,12 +38,14 @@ describe 'Search bar', js: true, feature: true do it 'hides after clicked' do filtered_search.set('a') find('.filtered-search-input-container .clear-search').click + expect(page).to have_css('.clear-search', visible: false) end it 'hides when there is no text' do filtered_search.set('a') filtered_search.set('') + expect(page).to have_css('.clear-search', visible: false) end @@ -57,10 +60,12 @@ describe 'Search bar', js: true, feature: true do original_size = page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size filtered_search.set('author') + expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(1) find('.filtered-search-input-container .clear-search').click filtered_search.click + expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(original_size) end @@ -70,10 +75,12 @@ describe 'Search bar', js: true, feature: true do hint_offset = get_left_style(hint_style) filtered_search.set('author:') + expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(0) find('.filtered-search-input-container .clear-search').click filtered_search.click + expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to be > 0 expect(get_left_style(page.find('#js-dropdown-hint')['style'])).to eq(hint_offset) end -- cgit v1.2.1 From 3e457f78cf6798042a1fc30790c24f935861182f Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 11 Jan 2017 21:12:03 -0500 Subject: Fix rubocop --- .../issues/filtered_search/filter_issues_spec.rb | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index 7cae7152aa2..ead43d6784a 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -107,12 +107,12 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by invalid author' do pending('to be tested, issue #26546') - fail + expect(true).to be(false) end it 'filters issues by multiple authors' do pending('to be tested, issue #26546') - fail + expect(true).to be(false) end end @@ -152,7 +152,7 @@ describe 'Filter issues', js: true, feature: true do it 'sorting' do pending('to be tested, issue #26546') - fail + expect(true).to be(false) end end @@ -176,12 +176,12 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by invalid assignee' do pending('to be tested, issue #26546') - fail + expect(true).to be(false) end it 'filters issues by multiple assignees' do pending('to be tested, issue #26546') - fail + expect(true).to be(false) end end @@ -222,7 +222,7 @@ describe 'Filter issues', js: true, feature: true do context 'sorting' do it 'sorts' do pending('to be tested, issue #26546') - fail + expect(true).to be(false) end end end @@ -247,7 +247,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by invalid label' do pending('to be tested, issue #26546') - fail + expect(true).to be(false) end it 'filters issues by multiple labels' do @@ -423,7 +423,7 @@ describe 'Filter issues', js: true, feature: true do context 'sorting' do it 'sorts' do pending('to be tested, issue #26546') - fail + expect(true).to be(false) end end end @@ -450,12 +450,12 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by invalid milestones' do pending('to be tested, issue #26546') - fail + expect(true).to be(false) end it 'filters issues by multiple milestones' do pending('to be tested, issue #26546') - fail + expect(true).to be(false) end it 'filters issues by milestone containing special characters' do @@ -517,7 +517,7 @@ describe 'Filter issues', js: true, feature: true do context 'sorting' do it 'sorts' do pending('to be tested, issue #26546') - fail + expect(true).to be(false) end end end -- cgit v1.2.1 From f986b4c4e529f4c2518f0ce37dc9dfcaa2f073a0 Mon Sep 17 00:00:00 2001 From: Horacio Sanson Date: Tue, 29 Nov 2016 02:41:29 +0900 Subject: Add support for PlantUML diagrams in Asciidoc. This MR enables rendering of PlantUML diagrams in Asciidoc documents. To add a PlantUML diagram all we need is to include a plantuml block like: ``` [plantuml, id="myDiagram", width="100px", height="100px"] -- bob -> alice : ping alice -> bob : pong -- ``` The plantuml block is substituted by an HTML img element with *src* pointing to an external PlantUML server. This MR also add a PlantUML integration section to the Administrator -> Settings page to configure the PlantUML rendering service and to enable/disable it. Closes: #17603 --- Gemfile | 25 +++--- Gemfile.lock | 3 + .../admin/application_settings_controller.rb | 2 + app/models/application_setting.rb | 6 ++ .../admin/application_settings/_form.html.haml | 17 ++++ changelogs/unreleased/asciidoctor-plantuml.yml | 4 + ...11_add_plant_uml_url_to_application_settings.rb | 12 +++ ...dd_plant_uml_enabled_to_application_settings.rb | 12 +++ db/schema.rb | 2 + .../img/integration/plantuml-example.png | Bin 0 -> 33034 bytes doc/administration/integration/plantuml.md | 87 +++++++++++++++++++++ doc/api/settings.md | 10 ++- doc/integration/README.md | 1 + lib/api/entities.rb | 2 + lib/api/settings.rb | 6 +- lib/gitlab/asciidoc.rb | 12 +++ lib/gitlab/current_settings.rb | 1 + spec/lib/gitlab/asciidoc_spec.rb | 4 + spec/requests/api/settings_spec.rb | 16 +++- 19 files changed, 206 insertions(+), 16 deletions(-) create mode 100644 changelogs/unreleased/asciidoctor-plantuml.yml create mode 100644 db/migrate/20161201001911_add_plant_uml_url_to_application_settings.rb create mode 100644 db/migrate/20161206003819_add_plant_uml_enabled_to_application_settings.rb create mode 100644 doc/administration/img/integration/plantuml-example.png create mode 100644 doc/administration/integration/plantuml.md diff --git a/Gemfile b/Gemfile index a8141abafc2..b43604bd253 100644 --- a/Gemfile +++ b/Gemfile @@ -99,18 +99,19 @@ gem 'unf', '~> 0.1.4' gem 'seed-fu', '~> 2.3.5' # Markdown and HTML processing -gem 'html-pipeline', '~> 1.11.0' -gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie' -gem 'gitlab-markup', '~> 1.5.1' -gem 'redcarpet', '~> 3.3.3' -gem 'RedCloth', '~> 4.3.2' -gem 'rdoc', '~> 4.2' -gem 'org-ruby', '~> 0.9.12' -gem 'creole', '~> 0.5.0' -gem 'wikicloth', '0.8.1' -gem 'asciidoctor', '~> 1.5.2' -gem 'rouge', '~> 2.0' -gem 'truncato', '~> 0.7.8' +gem 'html-pipeline', '~> 1.11.0' +gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie' +gem 'gitlab-markup', '~> 1.5.0' +gem 'redcarpet', '~> 3.3.3' +gem 'RedCloth', '~> 4.3.2' +gem 'rdoc', '~> 4.2' +gem 'org-ruby', '~> 0.9.12' +gem 'creole', '~> 0.5.0' +gem 'wikicloth', '0.8.1' +gem 'asciidoctor', '~> 1.5.2' +gem 'asciidoctor-plantuml', '0.0.6' +gem 'rouge', '~> 2.0' +gem 'truncato', '~> 0.7.8' # See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s # and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM diff --git a/Gemfile.lock b/Gemfile.lock index bdd591e008a..e2d7f94e571 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -54,6 +54,8 @@ GEM faraday_middleware-multi_json (~> 0.0) oauth2 (~> 1.0) asciidoctor (1.5.3) + asciidoctor-plantuml (0.0.6) + asciidoctor (~> 1.5) ast (2.3.0) attr_encrypted (3.0.3) encryptor (~> 3.0.0) @@ -841,6 +843,7 @@ DEPENDENCIES allocations (~> 1.0) asana (~> 0.4.0) asciidoctor (~> 1.5.2) + asciidoctor-plantuml (= 0.0.6) attr_encrypted (~> 3.0.0) awesome_print (~> 1.2.0) babosa (~> 1.0.2) diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index e34ba424497..1b4987dd738 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -101,6 +101,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :html_emails_enabled, :koding_enabled, :koding_url, + :plantuml_enabled, + :plantuml_url, :max_artifacts_size, :max_attachment_size, :metrics_enabled, diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index bf463a3b6bb..8fab77cda0a 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -68,6 +68,10 @@ class ApplicationSetting < ActiveRecord::Base presence: true, if: :koding_enabled + validates :plantuml_url, + presence: true, + if: :plantuml_enabled + validates :max_attachment_size, presence: true, numericality: { only_integer: true, greater_than: 0 } @@ -184,6 +188,8 @@ class ApplicationSetting < ActiveRecord::Base akismet_enabled: false, koding_enabled: false, koding_url: nil, + plantuml_enabled: false, + plantuml_url: nil, repository_checks_enabled: true, disabled_oauth_sign_in_sources: [], send_user_confirmation_email: false, diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 4612a7a058a..558bbe07b16 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -420,6 +420,23 @@ = succeed "." do = link_to "Koding administration documentation", help_page_path("administration/integration/koding") + %fieldset + %legend PlantUML + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :plantuml_enabled do + = f.check_box :plantuml_enabled + Enable PlantUML + .form-group + = f.label :plantuml_url, 'PlantUML URL', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :plantuml_url, class: 'form-control', placeholder: 'http://gitlab.your-plantuml-instance.com:8080' + .help-block + Allow rendering of + = link_to "PlantUML", "http://plantuml.com" + diagrams in Asciidoc documents using an external PlantUML service. + %fieldset %legend Usage statistics .form-group diff --git a/changelogs/unreleased/asciidoctor-plantuml.yml b/changelogs/unreleased/asciidoctor-plantuml.yml new file mode 100644 index 00000000000..ba6ef7c0800 --- /dev/null +++ b/changelogs/unreleased/asciidoctor-plantuml.yml @@ -0,0 +1,4 @@ +--- +title: Add support for PlantUML diagrams in AsciiDoc documents. +merge_request: 7810 +author: Horacio Sanson diff --git a/db/migrate/20161201001911_add_plant_uml_url_to_application_settings.rb b/db/migrate/20161201001911_add_plant_uml_url_to_application_settings.rb new file mode 100644 index 00000000000..b8d8742ae40 --- /dev/null +++ b/db/migrate/20161201001911_add_plant_uml_url_to_application_settings.rb @@ -0,0 +1,12 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddPlantUmlUrlToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :application_settings, :plantuml_url, :string + end +end diff --git a/db/migrate/20161206003819_add_plant_uml_enabled_to_application_settings.rb b/db/migrate/20161206003819_add_plant_uml_enabled_to_application_settings.rb new file mode 100644 index 00000000000..3677f978cc2 --- /dev/null +++ b/db/migrate/20161206003819_add_plant_uml_enabled_to_application_settings.rb @@ -0,0 +1,12 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddPlantUmlEnabledToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :application_settings, :plantuml_enabled, :boolean + end +end diff --git a/db/schema.rb b/db/schema.rb index f3bf7ced393..c58a886b0fa 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -107,6 +107,8 @@ ActiveRecord::Schema.define(version: 20170106172224) do t.integer "housekeeping_full_repack_period", default: 50, null: false t.integer "housekeeping_gc_period", default: 200, null: false t.boolean "html_emails_enabled", default: true + t.string "plantuml_url" + t.boolean "plantuml_enabled" end create_table "audit_events", force: :cascade do |t| diff --git a/doc/administration/img/integration/plantuml-example.png b/doc/administration/img/integration/plantuml-example.png new file mode 100644 index 00000000000..cb64eca1a8a Binary files /dev/null and b/doc/administration/img/integration/plantuml-example.png differ diff --git a/doc/administration/integration/plantuml.md b/doc/administration/integration/plantuml.md new file mode 100644 index 00000000000..e5cf592e0a6 --- /dev/null +++ b/doc/administration/integration/plantuml.md @@ -0,0 +1,87 @@ +# PlantUML & GitLab + +> [Introduced][ce-7810] in GitLab 8.16. + +When [PlantUML](http://plantuml.com) integration is enabled and configured in +GitLab we are able to create simple diagrams in AsciiDoc documents created in +snippets, wikis, and repos. + +## PlantUML Server + +Before you can enable PlantUML in GitLab; you need to set up your own PlantUML +server that will generate the diagrams. Installing and configuring your +own PlantUML server is easy in Debian/Ubuntu distributions using Tomcat. + +First you need to create a `plantuml.war` file from the source code: + +``` +sudo apt-get install graphviz openjdk-7-jdk git-core maven +git clone https://github.com/plantuml/plantuml-server.git +cd plantuml-server +mvn package +``` + +The above sequence of commands will generate a WAR file that can be deployed +using Tomcat: + +``` +sudo apt-get install tomcat7 +sudo cp target/plantuml.war /var/lib/tomcat7/webapps/plantuml.war +sudo chown tomcat7:tomcat7 /var/lib/tomcat7/webapps/plantuml.war +sudo service restart tomcat7 +``` + +Once the Tomcat service restarts the PlantUML service will be ready and +listening for requests on port 8080: + +``` +http://localhost:8080/plantuml +``` + +you can change these defaults by editing the `/etc/tomcat7/server.xml` file. + + +## GitLab + +You need to enable PlantUML integration from Settings under Admin Area. To do +that, login with an Admin account and do following: + + - in GitLab go to **Admin Area** and then **Settings** + - scroll to bottom of the page until PlantUML section + - check **Enable PlantUML** checkbox + - set the PlantUML instance as **PlantUML URL** + +## Creating Diagrams + +With PlantUML integration enabled and configured, we can start adding diagrams to +our AsciiDoc snippets, wikis and repos using blocks: + +``` +[plantuml, format="png", id="myDiagram", width="200px"] +-- +Bob->Alice : hello +Alice -> Bob : Go Away +-- +``` + +The above block will be converted to an HTML img tag with source pointing to the +PlantUML instance. If the PlantUML server is correctly configured, this should +render a nice diagram instead of the block: + +![PlantUML Integration](../img/integration/plantuml-example.png) + +Inside the block you can add any of the supported diagrams by PlantUML such as +[Sequence](http://plantuml.com/sequence-diagram), [Use Case](http://plantuml.com/use-case-diagram), +[Class](http://plantuml.com/class-diagram), [Activity](http://plantuml.com/activity-diagram-legacy), +[Component](http://plantuml.com/component-diagram), [State](http://plantuml.com/state-diagram), +and [Object](http://plantuml.com/object-diagram) diagrams. You do not need to use the PlantUML +diagram delimiters `@startuml`/`@enduml` as these are replaced by the AsciiDoc `plantuml` block. + +Some parameters can be added to the block definition: + + - *format*: Can be either `png` or `svg`. Note that `svg` is not supported by + all browsers so use with care. The default is `png`. + - *id*: A CSS id added to the diagram HTML tag. + - *width*: Width attribute added to the img tag. + - *height*: Height attribute added to the img tag. + diff --git a/doc/api/settings.md b/doc/api/settings.md index 0bd38a6e664..f86c7cc2f94 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -44,7 +44,9 @@ Example response: "repository_storage": "default", "repository_storages": ["default"], "koding_enabled": false, - "koding_url": null + "koding_url": null, + "plantuml_enabled": false, + "plantuml_url": null } ``` @@ -80,6 +82,8 @@ PUT /application/settings | `koding_enabled` | boolean | no | Enable Koding integration. Default is `false`. | | `koding_url` | string | yes (if `koding_enabled` is `true`) | The Koding instance URL for integration. | | `disabled_oauth_sign_in_sources` | Array of strings | no | Disabled OAuth sign-in sources | +| `plantuml_enabled` | boolean | no | Enable PlantUML integration. Default is `false`. | +| `plantuml_url` | string | yes (if `plantuml_enabled` is `true`) | The PlantUML instance URL for integration. | ```bash curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings?signup_enabled=false&default_project_visibility=1 @@ -112,6 +116,8 @@ Example response: "container_registry_token_expire_delay": 5, "repository_storage": "default", "koding_enabled": false, - "koding_url": null + "koding_url": null, + "plantuml_enabled": false, + "plantuml_url": null } ``` diff --git a/doc/integration/README.md b/doc/integration/README.md index ed843c0bfa9..e97430feb57 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -16,6 +16,7 @@ See the documentation below for details on how to configure these services. - [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users - [Akismet](akismet.md) Configure Akismet to stop spam - [Koding](../administration/integration/koding.md) Configure Koding to use IDE integration +- [PlantUML](../administration/integration/plantuml.md) Configure PlantUML to use diagrams in AsciiDoc documents. GitLab Enterprise Edition contains [advanced Jenkins support][jenkins]. diff --git a/lib/api/entities.rb b/lib/api/entities.rb index d2fadf6a3d0..885ce7d44bc 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -565,6 +565,8 @@ module API expose :repository_storages expose :koding_enabled expose :koding_url + expose :plantuml_enabled + expose :plantuml_url end class Release < Grape::Entity diff --git a/lib/api/settings.rb b/lib/api/settings.rb index 9eb9a105bde..c5eff16a5de 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -93,6 +93,10 @@ module API given koding_enabled: ->(val) { val } do requires :koding_url, type: String, desc: 'The Koding team URL' end + optional :plantuml_enabled, type: Boolean, desc: 'Enable PlantUML' + given plantuml_enabled: ->(val) { val } do + requires :plantuml_url, type: String, desc: 'The PlantUML server URL' + end optional :version_check_enabled, type: Boolean, desc: 'Let GitLab inform you when an update is available.' optional :email_author_in_body, type: Boolean, desc: 'Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead.' optional :html_emails_enabled, type: Boolean, desc: 'By default GitLab sends emails in HTML and plain text formats so mail clients can choose what format to use. Disable this option if you only want to send emails in plain text format.' @@ -114,7 +118,7 @@ module API :shared_runners_enabled, :max_artifacts_size, :container_registry_token_expire_delay, :metrics_enabled, :sidekiq_throttling_enabled, :recaptcha_enabled, :akismet_enabled, :admin_notification_email, :sentry_enabled, - :repository_storage, :repository_checks_enabled, :koding_enabled, + :repository_storage, :repository_checks_enabled, :koding_enabled, :plantuml_enabled, :version_check_enabled, :email_author_in_body, :html_emails_enabled, :housekeeping_enabled end diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb index fa234284361..0618107e2c3 100644 --- a/lib/gitlab/asciidoc.rb +++ b/lib/gitlab/asciidoc.rb @@ -1,5 +1,6 @@ require 'asciidoctor' require 'asciidoctor/converter/html5' +require "asciidoctor-plantuml" module Gitlab # Parser/renderer for the AsciiDoc format that uses Asciidoctor and filters @@ -29,6 +30,8 @@ module Gitlab ) asciidoc_opts[:attributes].unshift(*DEFAULT_ADOC_ATTRS) + plantuml_setup + html = ::Asciidoctor.convert(input, asciidoc_opts) html = Banzai.post_process(html, context) @@ -36,6 +39,15 @@ module Gitlab html.html_safe end + def self.plantuml_setup + Asciidoctor::PlantUml.configure do |conf| + conf.url = ApplicationSetting.current.plantuml_url + conf.svg_enable = ApplicationSetting.current.plantuml_enabled + conf.png_enable = ApplicationSetting.current.plantuml_enabled + conf.txt_enable = false + end + end + class Html5Converter < Asciidoctor::Converter::Html5Converter extend Asciidoctor::Converter::Config diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index 9d142f1b82e..2ff27e46d64 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -35,6 +35,7 @@ module Gitlab signin_enabled: Settings.gitlab['signin_enabled'], gravatar_enabled: Settings.gravatar['enabled'], koding_enabled: false, + plantuml_enabled: false, sign_in_text: nil, after_sign_up_text: nil, help_page_text: nil, diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb index f3843ca64ff..ba199917f5c 100644 --- a/spec/lib/gitlab/asciidoc_spec.rb +++ b/spec/lib/gitlab/asciidoc_spec.rb @@ -8,6 +8,10 @@ module Gitlab let(:html) { 'H2O' } context "without project" do + before do + allow_any_instance_of(ApplicationSetting).to receive(:current).and_return(::ApplicationSetting.create_from_defaults) + end + it "converts the input using Asciidoctor and default options" do expected_asciidoc_opts = { safe: :secure, diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index ad9d8a25af4..91e3c333a02 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -16,6 +16,8 @@ describe API::Settings, 'Settings', api: true do expect(json_response['repository_storage']).to eq('default') expect(json_response['koding_enabled']).to be_falsey expect(json_response['koding_url']).to be_nil + expect(json_response['plantuml_enabled']).to be_falsey + expect(json_response['plantuml_url']).to be_nil end end @@ -28,7 +30,8 @@ describe API::Settings, 'Settings', api: true do it "updates application settings" do put api("/application/settings", admin), - default_projects_limit: 3, signin_enabled: false, repository_storage: 'custom', koding_enabled: true, koding_url: 'http://koding.example.com' + default_projects_limit: 3, signin_enabled: false, repository_storage: 'custom', koding_enabled: true, koding_url: 'http://koding.example.com', + plantuml_enabled: true, plantuml_url: 'http://plantuml.example.com' expect(response).to have_http_status(200) expect(json_response['default_projects_limit']).to eq(3) expect(json_response['signin_enabled']).to be_falsey @@ -36,6 +39,8 @@ describe API::Settings, 'Settings', api: true do expect(json_response['repository_storages']).to eq(['custom']) expect(json_response['koding_enabled']).to be_truthy expect(json_response['koding_url']).to eq('http://koding.example.com') + expect(json_response['plantuml_enabled']).to be_truthy + expect(json_response['plantuml_url']).to eq('http://plantuml.example.com') end end @@ -47,5 +52,14 @@ describe API::Settings, 'Settings', api: true do expect(json_response['error']).to eq('koding_url is missing') end end + + context "missing plantuml_url value when plantuml_enabled is true" do + it "returns a blank parameter error message" do + put api("/application/settings", admin), plantuml_enabled: true + + expect(response).to have_http_status(400) + expect(json_response['error']).to eq('plantuml_url is missing') + end + end end end -- cgit v1.2.1 From 2b63470f4ef9e743ae94df39162a38089dca77aa Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 2 Jan 2017 13:01:27 +0530 Subject: Scroll to bottom on build completion if autoscroll was active --- app/assets/javascripts/build.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index bc13c46443a..207eb639780 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -83,6 +83,10 @@ return window.location.href.split("#")[0]; }; + Build.prototype.locationHash = function() { + return window.location.href.split("#")[1]; + }; + Build.prototype.getInitialBuildTrace = function() { var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped'] @@ -91,6 +95,9 @@ dataType: 'json', success: function(buildData) { $('.js-build-output').html(buildData.trace_html); + if (this.locationHash() === 'down-build-trace') { + $("html,body").scrollTop(this.$buildTrace.height()); + } if (removeRefreshStatuses.indexOf(buildData.status) >= 0) { this.$buildRefreshAnimation.remove(); return this.initScrollMonitor(); @@ -105,6 +112,8 @@ dataType: "json", success: (function(_this) { return function(log) { + var pageUrl; + if (log.state) { _this.state = log.state; } @@ -116,7 +125,12 @@ } return _this.checkAutoscroll(); } else if (log.status !== _this.buildStatus) { - return Turbolinks.visit(_this.pageUrl); + pageUrl = _this.pageUrl; + if (_this.$autoScrollStatus.data('state') === 'enabled') { + pageUrl += '#down-build-trace'; + } + + return Turbolinks.visit(pageUrl); } }; })(this) -- cgit v1.2.1 From a9e253b569d225823b0748bcf6e3855d99b53e14 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 2 Jan 2017 14:52:53 +0530 Subject: Feature changelog entry --- .../unreleased/26109-preserve-scroll-position-on-autoreload.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/26109-preserve-scroll-position-on-autoreload.yml diff --git a/changelogs/unreleased/26109-preserve-scroll-position-on-autoreload.yml b/changelogs/unreleased/26109-preserve-scroll-position-on-autoreload.yml new file mode 100644 index 00000000000..cde0d114d7c --- /dev/null +++ b/changelogs/unreleased/26109-preserve-scroll-position-on-autoreload.yml @@ -0,0 +1,4 @@ +--- +title: Scroll to bottom on build completion if autoscroll was active +merge_request: 8391 +author: -- cgit v1.2.1 From c69931f0f0199818689aba26519b14b6e462df83 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Sat, 7 Jan 2017 14:13:19 +0530 Subject: Use better approach to access hash --- app/assets/javascripts/build.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 207eb639780..e6831b77d1c 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -5,6 +5,7 @@ (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; var AUTO_SCROLL_OFFSET = 75; + var DOWN_BUILD_TRACE = 'down-build-trace'; this.Build = (function() { Build.interval = null; @@ -83,10 +84,6 @@ return window.location.href.split("#")[0]; }; - Build.prototype.locationHash = function() { - return window.location.href.split("#")[1]; - }; - Build.prototype.getInitialBuildTrace = function() { var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped'] @@ -95,7 +92,7 @@ dataType: 'json', success: function(buildData) { $('.js-build-output').html(buildData.trace_html); - if (this.locationHash() === 'down-build-trace') { + if (window.location.hash.substring(1) === DOWN_BUILD_TRACE) { $("html,body").scrollTop(this.$buildTrace.height()); } if (removeRefreshStatuses.indexOf(buildData.status) >= 0) { -- cgit v1.2.1 From c254d05953fbf31425f934a349a65e8866ccf07a Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Thu, 12 Jan 2017 10:49:49 -0500 Subject: Use string constant variable for `#down-build-trace` --- app/assets/javascripts/build.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index e6831b77d1c..fca47002870 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -5,7 +5,7 @@ (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; var AUTO_SCROLL_OFFSET = 75; - var DOWN_BUILD_TRACE = 'down-build-trace'; + var DOWN_BUILD_TRACE = '#down-build-trace'; this.Build = (function() { Build.interval = null; @@ -27,7 +27,7 @@ this.$autoScrollStatus = $('#autoscroll-status'); this.$autoScrollStatusText = this.$autoScrollStatus.find('.status-text'); this.$upBuildTrace = $('#up-build-trace'); - this.$downBuildTrace = $('#down-build-trace'); + this.$downBuildTrace = $(DOWN_BUILD_TRACE); this.$scrollTopBtn = $('#scroll-top'); this.$scrollBottomBtn = $('#scroll-bottom'); this.$buildRefreshAnimation = $('.js-build-refresh'); @@ -92,7 +92,7 @@ dataType: 'json', success: function(buildData) { $('.js-build-output').html(buildData.trace_html); - if (window.location.hash.substring(1) === DOWN_BUILD_TRACE) { + if (window.location.hash === DOWN_BUILD_TRACE) { $("html,body").scrollTop(this.$buildTrace.height()); } if (removeRefreshStatuses.indexOf(buildData.status) >= 0) { @@ -124,7 +124,7 @@ } else if (log.status !== _this.buildStatus) { pageUrl = _this.pageUrl; if (_this.$autoScrollStatus.data('state') === 'enabled') { - pageUrl += '#down-build-trace'; + pageUrl += DOWN_BUILD_TRACE; } return Turbolinks.visit(pageUrl); -- cgit v1.2.1 From e6408b1ccadf96a031c7dad51829706f6160e07a Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 12 Jan 2017 14:50:39 -0500 Subject: Add changelog for search-bar-first-iteration --- changelogs/unreleased/add-changelog-search-bar-first-iteration.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/add-changelog-search-bar-first-iteration.yml diff --git a/changelogs/unreleased/add-changelog-search-bar-first-iteration.yml b/changelogs/unreleased/add-changelog-search-bar-first-iteration.yml new file mode 100644 index 00000000000..4d83d744be7 --- /dev/null +++ b/changelogs/unreleased/add-changelog-search-bar-first-iteration.yml @@ -0,0 +1,4 @@ +--- +title: Search bar redesign first iteration +merge_request: 7345 +author: -- cgit v1.2.1 From bf4a042f6a810aef6f66b109c088f6a80d8c8a5d Mon Sep 17 00:00:00 2001 From: Tiago Botelho Date: Thu, 12 Jan 2017 15:49:48 -0500 Subject: fixes the issue --- spec/controllers/dashboard/todos_controller_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb index 288984cfba9..19fbc2f7748 100644 --- a/spec/controllers/dashboard/todos_controller_spec.rb +++ b/spec/controllers/dashboard/todos_controller_spec.rb @@ -12,7 +12,7 @@ describe Dashboard::TodosController do end context 'when using pagination' do - let(:last_page) { user.todos.page().total_pages } + let(:last_page) { user.todos.page.total_pages } let!(:issues) { create_list(:issue, 2, project: project, assignee: user) } before do -- cgit v1.2.1 From 100bbd95a3ea696f883c9ae8d2e29afaea6cecca Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 12 Jan 2017 16:03:02 -0500 Subject: Fixes pipeline status cell is too wide by adding missing classes in table head cells Adds mr id to changelog --- app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 | 12 ++++++------ changelogs/unreleased/26615-pipeline-status-cell.yml | 4 ++++ 2 files changed, 10 insertions(+), 6 deletions(-) create mode 100644 changelogs/unreleased/26615-pipeline-status-cell.yml diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 index 73627e9ba50..b2ed05503c9 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 @@ -73,12 +73,12 @@ - - - - - - + + + + + + diff --git a/changelogs/unreleased/26615-pipeline-status-cell.yml b/changelogs/unreleased/26615-pipeline-status-cell.yml new file mode 100644 index 00000000000..9a19b041e63 --- /dev/null +++ b/changelogs/unreleased/26615-pipeline-status-cell.yml @@ -0,0 +1,4 @@ +--- +title: Fixes pipeline status cell is too wide by adding missing classes in table head cells +merge_request: 8549 +author: -- cgit v1.2.1 From 6afa2d058ee8cb67fda7b2f7cba71304acf21948 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 12 Jan 2017 16:48:11 -0500 Subject: Update gitlab-markup back to 1.5.1 in Gemfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index b43604bd253..07ff500dfea 100644 --- a/Gemfile +++ b/Gemfile @@ -101,7 +101,7 @@ gem 'seed-fu', '~> 2.3.5' # Markdown and HTML processing gem 'html-pipeline', '~> 1.11.0' gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie' -gem 'gitlab-markup', '~> 1.5.0' +gem 'gitlab-markup', '~> 1.5.1' gem 'redcarpet', '~> 3.3.3' gem 'RedCloth', '~> 4.3.2' gem 'rdoc', '~> 4.2' -- cgit v1.2.1 From 6195ab754dcaac0930f01dae86d39adf9ac0fce9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 10 Jan 2017 15:41:31 +0100 Subject: Refactor allowed params in ServiceParams MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- app/controllers/concerns/service_params.rb | 79 +++++++++++++++++++++++------- 1 file changed, 60 insertions(+), 19 deletions(-) diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb index 549a8526715..d7f5a4e4682 100644 --- a/app/controllers/concerns/service_params.rb +++ b/app/controllers/concerns/service_params.rb @@ -1,31 +1,72 @@ module ServiceParams extend ActiveSupport::Concern - ALLOWED_PARAMS = [:title, :token, :type, :active, :api_key, :api_url, :api_version, :subdomain, - :room, :recipients, :project_url, :webhook, - :user_key, :device, :priority, :sound, :bamboo_url, :username, :password, - :build_key, :server, :teamcity_url, :drone_url, :build_type, - :description, :issues_url, :new_issue_url, :restrict_to_branch, :channel, - :colorize_messages, :channels, - # We're using `issues_events` and `merge_requests_events` - # in the view so we still need to explicitly state them - # here. `Service#event_names` would only give - # `issue_events` and `merge_request_events` (singular!) - # See app/helpers/services_helper.rb for how we - # make those event names plural as special case. - :issues_events, :confidential_issues_events, :merge_requests_events, - :notify_only_broken_builds, :notify_only_broken_pipelines, - :add_pusher, :send_from_committer_email, :disable_diffs, - :external_wiki_url, :notify, :color, - :server_host, :server_port, :default_irc_uri, :enable_ssl_verification, - :jira_issue_transition_id, :url, :project_key, :ca_pem, :namespace] + ALLOWED_PARAMS_CE = [ + :active, + :add_pusher, + :api_key, + :api_url, + :api_version, + :bamboo_url, + :build_key, + :build_type, + :ca_pem, + :channel, + :channels, + :color, + :colorize_messages, + :confidential_issues_events, + :default_irc_uri, + :description, + :device, + :disable_diffs, + :drone_url, + :enable_ssl_verification, + :external_wiki_url, + # We're using `issues_events` and `merge_requests_events` + # in the view so we still need to explicitly state them + # here. `Service#event_names` would only give + # `issue_events` and `merge_request_events` (singular!) + # See app/helpers/services_helper.rb for how we + # make those event names plural as special case. + :issues_events, + :issues_url, + :jira_issue_transition_id, + :merge_requests_events, + :namespace, + :new_issue_url, + :notify, + :notify_only_broken_builds, + :notify_only_broken_pipelines, + :password, + :priority, + :project_key, + :project_url, + :recipients, + :restrict_to_branch, + :room, + :send_from_committer_email, + :server, + :server_host, + :server_port, + :sound, + :subdomain, + :teamcity_url, + :title, + :token, + :type, + :url, + :user_key, + :username, + :webhook + ] # Parameters to ignore if no value is specified FILTER_BLANK_PARAMS = [:password] def service_params dynamic_params = @service.event_channel_names + @service.event_names - service_params = params.permit(:id, service: ALLOWED_PARAMS + dynamic_params) + service_params = params.permit(:id, service: ALLOWED_PARAMS_CE + dynamic_params) if service_params[:service].is_a?(Hash) FILTER_BLANK_PARAMS.each do |param| -- cgit v1.2.1 From 9e0d3906b931eb0f81c9cd9864891ca26cbefd44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 10 Jan 2017 15:57:09 +0100 Subject: Refactor authorized params in GroupsController MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- app/controllers/groups_controller.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index b61f4e9a2db..f81237db991 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -125,7 +125,11 @@ class GroupsController < Groups::ApplicationController end def group_params - params.require(:group).permit( + params.require(:group).permit(group_params_ce) + end + + def group_params_ce + [ :avatar, :description, :lfs_enabled, @@ -135,7 +139,7 @@ class GroupsController < Groups::ApplicationController :request_access_enabled, :share_with_group_lock, :visibility_level - ) + ] end def load_events -- cgit v1.2.1 From 0b5b3ec3a432842bc53c7226c6db98e484aeee50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 12 Jan 2017 17:37:14 -0500 Subject: Remove useless permission checks in Gitlab::Checks::ChangeAccess MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- lib/gitlab/checks/change_access.rb | 4 ++-- spec/lib/gitlab/checks/change_access_spec.rb | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb index 9c391fa92a3..273118135a9 100644 --- a/lib/gitlab/checks/change_access.rb +++ b/lib/gitlab/checks/change_access.rb @@ -30,9 +30,9 @@ module Gitlab return unless @branch_name return unless project.protected_branch?(@branch_name) - if forced_push? && user_access.cannot_do_action?(:force_push_code_to_protected_branches) + if forced_push? return "You are not allowed to force push code to a protected branch on this project." - elsif Gitlab::Git.blank_ref?(@newrev) && user_access.cannot_do_action?(:remove_protected_branches) + elsif Gitlab::Git.blank_ref?(@newrev) return "You are not allowed to delete protected branches from this project." end diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb index 39069b49978..98effecdbbc 100644 --- a/spec/lib/gitlab/checks/change_access_spec.rb +++ b/spec/lib/gitlab/checks/change_access_spec.rb @@ -56,7 +56,6 @@ describe Gitlab::Checks::ChangeAccess, lib: true do it 'returns an error if the user is not allowed to do forced pushes to protected branches' do expect(Gitlab::Checks::ForcePush).to receive(:force_push?).and_return(true) - expect(user_access).to receive(:can_do_action?).with(:force_push_code_to_protected_branches).and_return(false) expect(subject.status).to be(false) expect(subject.message).to eq('You are not allowed to force push code to a protected branch on this project.') @@ -88,8 +87,6 @@ describe Gitlab::Checks::ChangeAccess, lib: true do end it 'returns an error if the user is not allowed to delete protected branches' do - expect(user_access).to receive(:can_do_action?).with(:remove_protected_branches).and_return(false) - expect(subject.status).to be(false) expect(subject.message).to eq('You are not allowed to delete protected branches from this project.') end -- cgit v1.2.1 From e75b1f11057829964dd9c3aac3b0a0deb964707e Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 12 Jan 2017 22:31:02 +0000 Subject: Merge branch '24185-legacy-ci-status-reactive-cache' into 'security' Use ReactiveCaching to update external CI status asynchronously See https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/2055 --- .../projects/merge_requests_controller.rb | 4 - app/models/concerns/reactive_caching.rb | 46 ++++--- app/models/concerns/reactive_service.rb | 10 ++ app/models/project_services/bamboo_service.rb | 47 ++++--- app/models/project_services/buildkite_service.rb | 23 +++- app/models/project_services/ci_service.rb | 27 +--- app/models/project_services/drone_ci_service.rb | 76 +++-------- app/models/project_services/teamcity_service.rb | 73 +++++----- app/workers/reactive_caching_worker.rb | 4 +- .../24185-legacy-ci-status-reactive-cache.yml | 4 + .../models/project_services/bamboo_service_spec.rb | 149 +++++++++++++-------- .../project_services/buildkite_service_spec.rb | 77 +++++++++-- .../project_services/drone_ci_service_spec.rb | 72 ++++++++-- .../project_services/teamcity_service_spec.rb | 126 ++++++++++------- spec/support/reactive_caching_helpers.rb | 30 +++-- 15 files changed, 457 insertions(+), 311 deletions(-) create mode 100644 app/models/concerns/reactive_service.rb create mode 100644 changelogs/unreleased/24185-legacy-ci-status-reactive-cache.yml diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 6004e7d7115..aaebd4efa00 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -409,10 +409,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController else ci_service = @merge_request.source_project.try(:ci_service) status = ci_service.commit_status(merge_request.diff_head_sha, merge_request.source_branch) if ci_service - - if ci_service.respond_to?(:commit_coverage) - coverage = ci_service.commit_coverage(merge_request.diff_head_sha, merge_request.source_branch) - end end response = { diff --git a/app/models/concerns/reactive_caching.rb b/app/models/concerns/reactive_caching.rb index 944519a3070..2589215ad19 100644 --- a/app/models/concerns/reactive_caching.rb +++ b/app/models/concerns/reactive_caching.rb @@ -55,30 +55,30 @@ module ReactiveCaching self.reactive_cache_refresh_interval = 1.minute self.reactive_cache_lifetime = 10.minutes - def calculate_reactive_cache + def calculate_reactive_cache(*args) raise NotImplementedError end - def with_reactive_cache(&blk) - within_reactive_cache_lifetime do - data = Rails.cache.read(full_reactive_cache_key) + def with_reactive_cache(*args, &blk) + within_reactive_cache_lifetime(*args) do + data = Rails.cache.read(full_reactive_cache_key(*args)) yield data if data.present? end ensure - Rails.cache.write(full_reactive_cache_key('alive'), true, expires_in: self.class.reactive_cache_lifetime) - ReactiveCachingWorker.perform_async(self.class, id) + Rails.cache.write(alive_reactive_cache_key(*args), true, expires_in: self.class.reactive_cache_lifetime) + ReactiveCachingWorker.perform_async(self.class, id, *args) end - def clear_reactive_cache! - Rails.cache.delete(full_reactive_cache_key) + def clear_reactive_cache!(*args) + Rails.cache.delete(full_reactive_cache_key(*args)) end - def exclusively_update_reactive_cache! - locking_reactive_cache do - within_reactive_cache_lifetime do - enqueuing_update do - value = calculate_reactive_cache - Rails.cache.write(full_reactive_cache_key, value) + def exclusively_update_reactive_cache!(*args) + locking_reactive_cache(*args) do + within_reactive_cache_lifetime(*args) do + enqueuing_update(*args) do + value = calculate_reactive_cache(*args) + Rails.cache.write(full_reactive_cache_key(*args), value) end end end @@ -93,22 +93,26 @@ module ReactiveCaching ([prefix].flatten + qualifiers).join(':') end - def locking_reactive_cache - lease = Gitlab::ExclusiveLease.new(full_reactive_cache_key, timeout: reactive_cache_lease_timeout) + def alive_reactive_cache_key(*qualifiers) + full_reactive_cache_key(*(qualifiers + ['alive'])) + end + + def locking_reactive_cache(*args) + lease = Gitlab::ExclusiveLease.new(full_reactive_cache_key(*args), timeout: reactive_cache_lease_timeout) uuid = lease.try_obtain yield if uuid ensure - Gitlab::ExclusiveLease.cancel(full_reactive_cache_key, uuid) + Gitlab::ExclusiveLease.cancel(full_reactive_cache_key(*args), uuid) end - def within_reactive_cache_lifetime - yield if Rails.cache.read(full_reactive_cache_key('alive')) + def within_reactive_cache_lifetime(*args) + yield if Rails.cache.read(alive_reactive_cache_key(*args)) end - def enqueuing_update + def enqueuing_update(*args) yield ensure - ReactiveCachingWorker.perform_in(self.class.reactive_cache_refresh_interval, self.class, id) + ReactiveCachingWorker.perform_in(self.class.reactive_cache_refresh_interval, self.class, id, *args) end end end diff --git a/app/models/concerns/reactive_service.rb b/app/models/concerns/reactive_service.rb new file mode 100644 index 00000000000..e1f868a299b --- /dev/null +++ b/app/models/concerns/reactive_service.rb @@ -0,0 +1,10 @@ +module ReactiveService + extend ActiveSupport::Concern + + included do + include ReactiveCaching + + # Default cache key: class name + project_id + self.reactive_cache_key = ->(service) { [ service.class.model_name.singular, service.project_id ] } + end +end diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index b5c76e4d4fe..4819bdbef8c 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -1,4 +1,6 @@ class BambooService < CiService + include ReactiveService + prop_accessor :bamboo_url, :build_key, :username, :password validates :bamboo_url, presence: true, url: true, if: :activated? @@ -58,31 +60,46 @@ class BambooService < CiService %w(push) end - def build_info(sha) - @response = get_path("rest/api/latest/result?label=#{sha}") + def build_page(sha, ref) + with_reactive_cache(sha, ref) {|cached| cached[:build_page] } end - def build_page(sha, ref) - build_info(sha) if @response.nil? || !@response.code + def commit_status(sha, ref) + with_reactive_cache(sha, ref) {|cached| cached[:commit_status] } + end - if @response.code != 200 || @response['results']['results']['size'] == '0' + def execute(data) + return unless supported_events.include?(data[:object_kind]) + + get_path("updateAndBuild.action?buildKey=#{build_key}") + end + + def calculate_reactive_cache(sha, ref) + response = get_path("rest/api/latest/result?label=#{sha}") + + { build_page: read_build_page(response), commit_status: read_commit_status(response) } + end + + private + + def read_build_page(response) + if response.code != 200 || response['results']['results']['size'] == '0' # If actual build link can't be determined, send user to build summary page. URI.join("#{bamboo_url}/", "browse/#{build_key}").to_s else # If actual build link is available, go to build result page. - result_key = @response['results']['results']['result']['planResultKey']['key'] + result_key = response['results']['results']['result']['planResultKey']['key'] URI.join("#{bamboo_url}/", "browse/#{result_key}").to_s end end - def commit_status(sha, ref) - build_info(sha) if @response.nil? || !@response.code - return :error unless @response.code == 200 || @response.code == 404 + def read_commit_status(response) + return :error unless response.code == 200 || response.code == 404 - status = if @response.code == 404 || @response['results']['results']['size'] == '0' + status = if response.code == 404 || response['results']['results']['size'] == '0' 'Pending' else - @response['results']['results']['result']['buildState'] + response['results']['results']['result']['buildState'] end if status.include?('Success') @@ -96,14 +113,6 @@ class BambooService < CiService end end - def execute(data) - return unless supported_events.include?(data[:object_kind]) - - get_path("updateAndBuild.action?buildKey=#{build_key}") - end - - private - def build_url(path) URI.join("#{bamboo_url}/", path).to_s end diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb index fe6d7aabb22..e77942d8f3c 100644 --- a/app/models/project_services/buildkite_service.rb +++ b/app/models/project_services/buildkite_service.rb @@ -1,6 +1,8 @@ require "addressable/uri" class BuildkiteService < CiService + include ReactiveService + ENDPOINT = "https://buildkite.com" prop_accessor :project_url, :token @@ -33,13 +35,7 @@ class BuildkiteService < CiService end def commit_status(sha, ref) - response = HTTParty.get(commit_status_path(sha), verify: false) - - if response.code == 200 && response['status'] - response['status'] - else - :error - end + with_reactive_cache(sha, ref) {|cached| cached[:commit_status] } end def commit_status_path(sha) @@ -78,6 +74,19 @@ class BuildkiteService < CiService ] end + def calculate_reactive_cache(sha, ref) + response = HTTParty.get(commit_status_path(sha), verify: false) + + status = + if response.code == 200 && response['status'] + response['status'] + else + :error + end + + { commit_status: status } + end + private def webhook_token diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb index 596c00705ad..4de0106707e 100644 --- a/app/models/project_services/ci_service.rb +++ b/app/models/project_services/ci_service.rb @@ -12,15 +12,7 @@ class CiService < Service %w(push) end - def merge_request_page(iid, sha, ref) - commit_page(sha, ref) - end - - def commit_page(sha, ref) - build_page(sha, ref) - end - - # Return complete url to merge_request page + # Return complete url to build page # # Ex. # http://jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c @@ -29,23 +21,6 @@ class CiService < Service # implement inside child end - # Return string with build status or :error symbol - # - # Allowed states: 'success', 'failed', 'running', 'pending', 'skipped' - # - # - # Ex. - # @service.merge_request_status(9, '13be4ac', 'dev') - # # => 'success' - # - # @service.merge_request_status(10, '2abe4ac', 'dev) - # # => 'running' - # - # - def merge_request_status(iid, sha, ref) - commit_status(sha, ref) - end - # Return string with build status or :error symbol # # Allowed states: 'success', 'failed', 'running', 'pending', 'skipped' diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb index adc78a427ee..4bbbebf54cb 100644 --- a/app/models/project_services/drone_ci_service.rb +++ b/app/models/project_services/drone_ci_service.rb @@ -1,4 +1,6 @@ class DroneCiService < CiService + include ReactiveService + prop_accessor :drone_url, :token boolean_accessor :enable_ssl_verification @@ -34,14 +36,6 @@ class DroneCiService < CiService %w(push merge_request tag_push) end - def merge_request_status_path(iid, sha = nil, ref = nil) - url = [drone_url, - "gitlab/#{project.namespace.path}/#{project.path}/pulls/#{iid}", - "?access_token=#{token}"] - - URI.join(*url).to_s - end - def commit_status_path(sha, ref) url = [drone_url, "gitlab/#{project.namespace.path}/#{project.path}/commits/#{sha}", @@ -50,54 +44,34 @@ class DroneCiService < CiService URI.join(*url).to_s end - def merge_request_status(iid, sha, ref) - response = HTTParty.get(merge_request_status_path(iid), verify: enable_ssl_verification) - - if response.code == 200 and response['status'] - case response['status'] - when 'killed' - :canceled - when 'failure', 'error' - # Because drone return error if some test env failed - :failed - else - response["status"] - end - else - :error - end - rescue Errno::ECONNREFUSED - :error + def commit_status(sha, ref) + with_reactive_cache(sha, ref) {|cached| cached[:commit_status] } end - def commit_status(sha, ref) + def calculate_reactive_cache(sha, ref) response = HTTParty.get(commit_status_path(sha, ref), verify: enable_ssl_verification) - if response.code == 200 and response['status'] - case response['status'] - when 'killed' - :canceled - when 'failure', 'error' - # Because drone return error if some test env failed - :failed + status = + if response.code == 200 and response['status'] + case response['status'] + when 'killed' + :canceled + when 'failure', 'error' + # Because drone return error if some test env failed + :failed + else + response["status"] + end else - response["status"] + :error end - else - :error - end - rescue Errno::ECONNREFUSED - :error - end - def merge_request_page(iid, sha, ref) - url = [drone_url, - "gitlab/#{project.namespace.path}/#{project.path}/redirect/pulls/#{iid}"] - - URI.join(*url).to_s + { commit_status: status } + rescue Errno::ECONNREFUSED + { commit_status: :error } end - def commit_page(sha, ref) + def build_page(sha, ref) url = [drone_url, "gitlab/#{project.namespace.path}/#{project.path}/redirect/commits/#{sha}", "?branch=#{URI::encode(ref.to_s)}"] @@ -105,14 +79,6 @@ class DroneCiService < CiService URI.join(*url).to_s end - def commit_coverage(sha, ref) - nil - end - - def build_page(sha, ref) - commit_page(sha, ref) - end - def title 'Drone CI' end diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index a4a967c9bc9..6726082048f 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -1,4 +1,6 @@ class TeamcityService < CiService + include ReactiveService + prop_accessor :teamcity_url, :build_type, :username, :password validates :teamcity_url, presence: true, url: true, if: :activated? @@ -61,43 +63,18 @@ class TeamcityService < CiService ] end - def build_info(sha) - @response = get_path("httpAuth/app/rest/builds/branch:unspecified:any,number:#{sha}") - end - def build_page(sha, ref) - build_info(sha) if @response.nil? || !@response.code - - if @response.code != 200 - # If actual build link can't be determined, - # send user to build summary page. - build_url("viewLog.html?buildTypeId=#{build_type}") - else - # If actual build link is available, go to build result page. - built_id = @response['build']['id'] - build_url("viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}") - end + with_reactive_cache(sha, ref) {|cached| cached[:build_page] } end def commit_status(sha, ref) - build_info(sha) if @response.nil? || !@response.code - return :error unless @response.code == 200 || @response.code == 404 + with_reactive_cache(sha, ref) {|cached| cached[:commit_status] } + end - status = if @response.code == 404 - 'Pending' - else - @response['build']['status'] - end + def calculate_reactive_cache(sha, ref) + response = get_path("httpAuth/app/rest/builds/branch:unspecified:any,number:#{sha}") - if status.include?('SUCCESS') - 'success' - elsif status.include?('FAILURE') - 'failed' - elsif status.include?('Pending') - 'pending' - else - :error - end + { build_page: read_build_page(response), commit_status: read_commit_status(response) } end def execute(data) @@ -122,6 +99,40 @@ class TeamcityService < CiService private + def read_build_page(response) + if response.code != 200 + # If actual build link can't be determined, + # send user to build summary page. + build_url("viewLog.html?buildTypeId=#{build_type}") + else + # If actual build link is available, go to build result page. + built_id = response['build']['id'] + build_url("viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}") + end + end + + def read_commit_status(response) + return :error unless response.code == 200 || response.code == 404 + + status = if response.code == 404 + 'Pending' + else + response['build']['status'] + end + + return :error unless status.present? + + if status.include?('SUCCESS') + 'success' + elsif status.include?('FAILURE') + 'failed' + elsif status.include?('Pending') + 'pending' + else + :error + end + end + def build_url(path) URI.join("#{teamcity_url}/", path).to_s end diff --git a/app/workers/reactive_caching_worker.rb b/app/workers/reactive_caching_worker.rb index 9af9dae04f0..18b8daf4e1e 100644 --- a/app/workers/reactive_caching_worker.rb +++ b/app/workers/reactive_caching_worker.rb @@ -2,7 +2,7 @@ class ReactiveCachingWorker include Sidekiq::Worker include DedicatedSidekiqQueue - def perform(class_name, id) + def perform(class_name, id, *args) klass = begin Kernel.const_get(class_name) rescue NameError @@ -10,6 +10,6 @@ class ReactiveCachingWorker end return unless klass - klass.find_by(id: id).try(:exclusively_update_reactive_cache!) + klass.find_by(id: id).try(:exclusively_update_reactive_cache!, *args) end end diff --git a/changelogs/unreleased/24185-legacy-ci-status-reactive-cache.yml b/changelogs/unreleased/24185-legacy-ci-status-reactive-cache.yml new file mode 100644 index 00000000000..09ff63a44fb --- /dev/null +++ b/changelogs/unreleased/24185-legacy-ci-status-reactive-cache.yml @@ -0,0 +1,4 @@ +--- +title: Query external CI statuses in the background +merge_request: +author: diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb index d7e1a4e3b6c..497a626a418 100644 --- a/spec/models/project_services/bamboo_service_spec.rb +++ b/spec/models/project_services/bamboo_service_spec.rb @@ -1,14 +1,28 @@ require 'spec_helper' -describe BambooService, models: true do +describe BambooService, models: true, caching: true do + include ReactiveCachingHelpers + + let(:bamboo_url) { 'http://gitlab.com/bamboo' } + + subject(:service) do + described_class.create( + project: create(:empty_project), + properties: { + bamboo_url: bamboo_url, + username: 'mic', + password: 'password', + build_key: 'foo' + } + ) + end + describe 'Associations' do it { is_expected.to belong_to :project } it { is_expected.to have_one :service_hook } end describe 'Validations' do - subject { service } - context 'when service is active' do before { subject.active = true } @@ -103,90 +117,103 @@ describe BambooService, models: true do end describe '#build_page' do - it 'returns a specific URL when status is 500' do - stub_request(status: 500) + it 'returns the contents of the reactive cache' do + stub_reactive_cache(service, { build_page: 'foo' }, 'sha', 'ref') - expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/bamboo/browse/foo') + expect(service.build_page('sha', 'ref')).to eq('foo') end + end - it 'returns a specific URL when response has no results' do - stub_request(body: %Q({"results":{"results":{"size":"0"}}})) + describe '#commit_status' do + it 'returns the contents of the reactive cache' do + stub_reactive_cache(service, { commit_status: 'foo' }, 'sha', 'ref') - expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/bamboo/browse/foo') + expect(service.commit_status('sha', 'ref')).to eq('foo') end + end - it 'returns a build URL when bamboo_url has no trailing slash' do - stub_request(body: %Q({"results":{"results":{"result":{"planResultKey":{"key":"42"}}}}})) + describe '#calculate_reactive_cache' do + context '#build_page' do + subject { service.calculate_reactive_cache('123', 'unused')[:build_page] } - expect(service(bamboo_url: 'http://gitlab.com/bamboo').build_page('123', 'unused')).to eq('http://gitlab.com/bamboo/browse/42') - end + it 'returns a specific URL when status is 500' do + stub_request(status: 500) - it 'returns a build URL when bamboo_url has a trailing slash' do - stub_request(body: %Q({"results":{"results":{"result":{"planResultKey":{"key":"42"}}}}})) + is_expected.to eq('http://gitlab.com/bamboo/browse/foo') + end - expect(service(bamboo_url: 'http://gitlab.com/bamboo/').build_page('123', 'unused')).to eq('http://gitlab.com/bamboo/browse/42') - end - end + it 'returns a specific URL when response has no results' do + stub_request(body: bamboo_response(size: 0)) - describe '#commit_status' do - it 'sets commit status to :error when status is 500' do - stub_request(status: 500) + is_expected.to eq('http://gitlab.com/bamboo/browse/foo') + end - expect(service.commit_status('123', 'unused')).to eq(:error) - end + it 'returns a build URL when bamboo_url has no trailing slash' do + stub_request(body: bamboo_response) - it 'sets commit status to "pending" when status is 404' do - stub_request(status: 404) + is_expected.to eq('http://gitlab.com/bamboo/browse/42') + end - expect(service.commit_status('123', 'unused')).to eq('pending') - end + context 'bamboo_url has trailing slash' do + let(:bamboo_url) { 'http://gitlab.com/bamboo/' } - it 'sets commit status to "pending" when response has no results' do - stub_request(body: %Q({"results":{"results":{"size":"0"}}})) + it 'returns a build URL' do + stub_request(body: bamboo_response) - expect(service.commit_status('123', 'unused')).to eq('pending') + is_expected.to eq('http://gitlab.com/bamboo/browse/42') + end + end end - it 'sets commit status to "success" when build state contains Success' do - stub_request(build_state: 'YAY Success!') + context '#commit_status' do + subject { service.calculate_reactive_cache('123', 'unused')[:commit_status] } - expect(service.commit_status('123', 'unused')).to eq('success') - end + it 'sets commit status to :error when status is 500' do + stub_request(status: 500) - it 'sets commit status to "failed" when build state contains Failed' do - stub_request(build_state: 'NO Failed!') + is_expected.to eq(:error) + end - expect(service.commit_status('123', 'unused')).to eq('failed') - end + it 'sets commit status to "pending" when status is 404' do + stub_request(status: 404) - it 'sets commit status to "pending" when build state contains Pending' do - stub_request(build_state: 'NO Pending!') + is_expected.to eq('pending') + end - expect(service.commit_status('123', 'unused')).to eq('pending') - end + it 'sets commit status to "pending" when response has no results' do + stub_request(body: %Q({"results":{"results":{"size":"0"}}})) - it 'sets commit status to :error when build state is unknown' do - stub_request(build_state: 'FOO BAR!') + is_expected.to eq('pending') + end - expect(service.commit_status('123', 'unused')).to eq(:error) - end - end + it 'sets commit status to "success" when build state contains Success' do + stub_request(body: bamboo_response(build_state: 'YAY Success!')) - def service(bamboo_url: 'http://gitlab.com/bamboo') - described_class.create( - project: create(:empty_project), - properties: { - bamboo_url: bamboo_url, - username: 'mic', - password: 'password', - build_key: 'foo' - } - ) + is_expected.to eq('success') + end + + it 'sets commit status to "failed" when build state contains Failed' do + stub_request(body: bamboo_response(build_state: 'NO Failed!')) + + is_expected.to eq('failed') + end + + it 'sets commit status to "pending" when build state contains Pending' do + stub_request(body: bamboo_response(build_state: 'NO Pending!')) + + is_expected.to eq('pending') + end + + it 'sets commit status to :error when build state is unknown' do + stub_request(body: bamboo_response(build_state: 'FOO BAR!')) + + is_expected.to eq(:error) + end + end end - def stub_request(status: 200, body: nil, build_state: 'success') + def stub_request(status: 200, body: nil) bamboo_full_url = 'http://mic:password@gitlab.com/bamboo/rest/api/latest/result?label=123&os_authType=basic' - body ||= %Q({"results":{"results":{"result":{"buildState":"#{build_state}"}}}}) WebMock.stub_request(:get, bamboo_full_url).to_return( status: status, @@ -194,4 +221,8 @@ describe BambooService, models: true do body: body ) end + + def bamboo_response(result_key: 42, build_state: 'success', size: 1) + %Q({"results":{"results":{"size":"#{size}","result":{"buildState":"#{build_state}","planResultKey":{"key":"#{result_key}"}}}}}) + end end diff --git a/spec/models/project_services/buildkite_service_spec.rb b/spec/models/project_services/buildkite_service_spec.rb index 6f65beb79d0..dbd23ff5491 100644 --- a/spec/models/project_services/buildkite_service_spec.rb +++ b/spec/models/project_services/buildkite_service_spec.rb @@ -1,6 +1,21 @@ require 'spec_helper' -describe BuildkiteService, models: true do +describe BuildkiteService, models: true, caching: true do + include ReactiveCachingHelpers + + let(:project) { create(:empty_project) } + + subject(:service) do + described_class.create( + project: project, + properties: { + service_hook: true, + project_url: 'https://buildkite.com/account-name/example-project', + token: 'secret-sauce-webhook-token:secret-sauce-status-token' + } + ) + end + describe 'Associations' do it { is_expected.to belong_to :project } it { is_expected.to have_one :service_hook } @@ -25,21 +40,12 @@ describe BuildkiteService, models: true do describe 'commits methods' do before do - @project = Project.new - allow(@project).to receive(:default_branch).and_return('default-brancho') - - @service = BuildkiteService.new - allow(@service).to receive_messages( - project: @project, - service_hook: true, - project_url: 'https://buildkite.com/account-name/example-project', - token: 'secret-sauce-webhook-token:secret-sauce-status-token' - ) + allow(project).to receive(:default_branch).and_return('default-brancho') end describe '#webhook_url' do it 'returns the webhook url' do - expect(@service.webhook_url).to eq( + expect(service.webhook_url).to eq( 'https://webhook.buildkite.com/deliver/secret-sauce-webhook-token' ) end @@ -47,7 +53,7 @@ describe BuildkiteService, models: true do describe '#commit_status_path' do it 'returns the correct status page' do - expect(@service.commit_status_path('2ab7834c')).to eq( + expect(service.commit_status_path('2ab7834c')).to eq( 'https://gitlab.buildkite.com/status/secret-sauce-status-token.json?commit=2ab7834c' ) end @@ -55,10 +61,53 @@ describe BuildkiteService, models: true do describe '#build_page' do it 'returns the correct build page' do - expect(@service.build_page('2ab7834c', nil)).to eq( + expect(service.build_page('2ab7834c', nil)).to eq( 'https://buildkite.com/account-name/example-project/builds?commit=2ab7834c' ) end end + + describe '#commit_status' do + it 'returns the contents of the reactive cache' do + stub_reactive_cache(service, { commit_status: 'foo' }, 'sha', 'ref') + + expect(service.commit_status('sha', 'ref')).to eq('foo') + end + end + + describe '#calculate_reactive_cache' do + context '#commit_status' do + subject { service.calculate_reactive_cache('123', 'unused')[:commit_status] } + + it 'sets commit status to :error when status is 500' do + stub_request(status: 500) + + is_expected.to eq(:error) + end + + it 'sets commit status to :error when status is 404' do + stub_request(status: 404) + + is_expected.to eq(:error) + end + + it 'passes through build status untouched when status is 200' do + stub_request(body: %Q({"status":"Great Success"})) + + is_expected.to eq('Great Success') + end + end + end + end + + def stub_request(status: 200, body: nil) + body ||= %Q({"status":"success"}) + buildkite_full_url = 'https://gitlab.buildkite.com/status/secret-sauce-status-token.json?commit=123' + + WebMock.stub_request(:get, buildkite_full_url).to_return( + status: status, + headers: { 'Content-Type' => 'application/json' }, + body: body + ) end end diff --git a/spec/models/project_services/drone_ci_service_spec.rb b/spec/models/project_services/drone_ci_service_spec.rb index f13bb1e8adf..42c2ed668bc 100644 --- a/spec/models/project_services/drone_ci_service_spec.rb +++ b/spec/models/project_services/drone_ci_service_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' -describe DroneCiService, models: true do +describe DroneCiService, models: true, caching: true do + include ReactiveCachingHelpers + describe 'associations' do it { is_expected.to belong_to(:project) } it { is_expected.to have_one(:service_hook) } @@ -33,6 +35,10 @@ describe DroneCiService, models: true do let(:token) { 'secret' } let(:iid) { rand(1..9999) } + # URL's + let(:build_page) { "#{drone_url}/gitlab/#{path}/redirect/commits/#{sha}?branch=#{branch}" } + let(:commit_status_path) { "#{drone_url}/gitlab/#{path}/commits/#{sha}?branch=#{branch}&access_token=#{token}" } + before(:each) do allow(drone).to receive_messages( project_id: project.id, @@ -42,22 +48,66 @@ describe DroneCiService, models: true do token: token ) end + + def stub_request(status: 200, body: nil) + body ||= %Q({"status":"success"}) + + WebMock.stub_request(:get, commit_status_path).to_return( + status: status, + headers: { 'Content-Type' => 'application/json' }, + body: body + ) + end end describe "service page/path methods" do include_context :drone_ci_service - # URL's - let(:commit_page) { "#{drone_url}/gitlab/#{path}/redirect/commits/#{sha}?branch=#{branch}" } - let(:merge_request_page) { "#{drone_url}/gitlab/#{path}/redirect/pulls/#{iid}" } - let(:commit_status_path) { "#{drone_url}/gitlab/#{path}/commits/#{sha}?branch=#{branch}&access_token=#{token}" } - let(:merge_request_status_path) { "#{drone_url}/gitlab/#{path}/pulls/#{iid}?access_token=#{token}" } - - it { expect(drone.build_page(sha, branch)).to eq(commit_page) } - it { expect(drone.commit_page(sha, branch)).to eq(commit_page) } - it { expect(drone.merge_request_page(iid, sha, branch)).to eq(merge_request_page) } + it { expect(drone.build_page(sha, branch)).to eq(build_page) } it { expect(drone.commit_status_path(sha, branch)).to eq(commit_status_path) } - it { expect(drone.merge_request_status_path(iid, sha, branch)).to eq(merge_request_status_path) } + end + + describe '#commit_status' do + include_context :drone_ci_service + + it 'returns the contents of the reactive cache' do + stub_reactive_cache(drone, { commit_status: 'foo' }, 'sha', 'ref') + + expect(drone.commit_status('sha', 'ref')).to eq('foo') + end + end + + describe '#calculate_reactive_cache' do + include_context :drone_ci_service + + context '#commit_status' do + subject { drone.calculate_reactive_cache(sha, branch)[:commit_status] } + + it 'sets commit status to :error when status is 500' do + stub_request(status: 500) + + is_expected.to eq(:error) + end + + it 'sets commit status to :error when status is 404' do + stub_request(status: 404) + + is_expected.to eq(:error) + end + + { "killed" => :canceled, + "failure" => :failed, + "error" => :failed, + "success" => "success", + }.each do |drone_status, our_status| + + it "sets commit status to #{our_status.inspect} when returned status is #{drone_status.inspect}" do + stub_request(body: %Q({"status":"#{drone_status}"})) + + is_expected.to eq(our_status) + end + end + end end describe "execute" do diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb index f7e878844dc..a1edd083aa1 100644 --- a/spec/models/project_services/teamcity_service_spec.rb +++ b/spec/models/project_services/teamcity_service_spec.rb @@ -1,14 +1,28 @@ require 'spec_helper' -describe TeamcityService, models: true do +describe TeamcityService, models: true, caching: true do + include ReactiveCachingHelpers + + let(:teamcity_url) { 'http://gitlab.com/teamcity' } + + subject(:service) do + described_class.create( + project: create(:empty_project), + properties: { + teamcity_url: teamcity_url, + username: 'mic', + password: 'password', + build_type: 'foo' + } + ) + end + describe 'Associations' do it { is_expected.to belong_to :project } it { is_expected.to have_one :service_hook } end describe 'Validations' do - subject { service } - context 'when service is active' do before { subject.active = true } @@ -103,73 +117,87 @@ describe TeamcityService, models: true do end describe '#build_page' do - it 'returns a specific URL when status is 500' do - stub_request(status: 500) + it 'returns the contents of the reactive cache' do + stub_reactive_cache(service, { build_page: 'foo' }, 'sha', 'ref') - expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/teamcity/viewLog.html?buildTypeId=foo') + expect(service.build_page('sha', 'ref')).to eq('foo') end + end - it 'returns a build URL when teamcity_url has no trailing slash' do - stub_request(body: %Q({"build":{"id":"666"}})) + describe '#commit_status' do + it 'returns the contents of the reactive cache' do + stub_reactive_cache(service, { commit_status: 'foo' }, 'sha', 'ref') - expect(service(teamcity_url: 'http://gitlab.com/teamcity').build_page('123', 'unused')).to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo') + expect(service.commit_status('sha', 'ref')).to eq('foo') end + end - it 'returns a build URL when teamcity_url has a trailing slash' do - stub_request(body: %Q({"build":{"id":"666"}})) + describe '#calculate_reactive_cache' do + context 'build_page' do + subject { service.calculate_reactive_cache('123', 'unused')[:build_page] } - expect(service(teamcity_url: 'http://gitlab.com/teamcity/').build_page('123', 'unused')).to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo') - end - end + it 'returns a specific URL when status is 500' do + stub_request(status: 500) - describe '#commit_status' do - it 'sets commit status to :error when status is 500' do - stub_request(status: 500) + is_expected.to eq('http://gitlab.com/teamcity/viewLog.html?buildTypeId=foo') + end - expect(service.commit_status('123', 'unused')).to eq(:error) - end + it 'returns a build URL when teamcity_url has no trailing slash' do + stub_request(body: %Q({"build":{"id":"666"}})) - it 'sets commit status to "pending" when status is 404' do - stub_request(status: 404) + is_expected.to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo') + end - expect(service.commit_status('123', 'unused')).to eq('pending') - end + context 'teamcity_url has trailing slash' do + let(:teamcity_url) { 'http://gitlab.com/teamcity/' } - it 'sets commit status to "success" when build status contains SUCCESS' do - stub_request(build_status: 'YAY SUCCESS!') + it 'returns a build URL' do + stub_request(body: %Q({"build":{"id":"666"}})) - expect(service.commit_status('123', 'unused')).to eq('success') + is_expected.to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo') + end + end end - it 'sets commit status to "failed" when build status contains FAILURE' do - stub_request(build_status: 'NO FAILURE!') + context 'commit_status' do + subject { service.calculate_reactive_cache('123', 'unused')[:commit_status] } - expect(service.commit_status('123', 'unused')).to eq('failed') - end + it 'sets commit status to :error when status is 500' do + stub_request(status: 500) - it 'sets commit status to "pending" when build status contains Pending' do - stub_request(build_status: 'NO Pending!') + is_expected.to eq(:error) + end - expect(service.commit_status('123', 'unused')).to eq('pending') - end + it 'sets commit status to "pending" when status is 404' do + stub_request(status: 404) - it 'sets commit status to :error when build status is unknown' do - stub_request(build_status: 'FOO BAR!') + is_expected.to eq('pending') + end - expect(service.commit_status('123', 'unused')).to eq(:error) - end - end + it 'sets commit status to "success" when build status contains SUCCESS' do + stub_request(build_status: 'YAY SUCCESS!') - def service(teamcity_url: 'http://gitlab.com/teamcity') - described_class.create( - project: create(:empty_project), - properties: { - teamcity_url: teamcity_url, - username: 'mic', - password: 'password', - build_type: 'foo' - } - ) + is_expected.to eq('success') + end + + it 'sets commit status to "failed" when build status contains FAILURE' do + stub_request(build_status: 'NO FAILURE!') + + is_expected.to eq('failed') + end + + it 'sets commit status to "pending" when build status contains Pending' do + stub_request(build_status: 'NO Pending!') + + is_expected.to eq('pending') + end + + it 'sets commit status to :error when build status is unknown' do + stub_request(build_status: 'FOO BAR!') + + is_expected.to eq(:error) + end + end end def stub_request(status: 200, body: nil, build_status: 'success') diff --git a/spec/support/reactive_caching_helpers.rb b/spec/support/reactive_caching_helpers.rb index 279db3c5748..98eb57f8b54 100644 --- a/spec/support/reactive_caching_helpers.rb +++ b/spec/support/reactive_caching_helpers.rb @@ -3,31 +3,35 @@ module ReactiveCachingHelpers ([subject.class.reactive_cache_key.call(subject)].flatten + qualifiers).join(':') end - def stub_reactive_cache(subject = nil, data = nil) + def alive_reactive_cache_key(subject, *qualifiers) + reactive_cache_key(subject, *(qualifiers + ['alive'])) + end + + def stub_reactive_cache(subject = nil, data = nil, *qualifiers) allow(ReactiveCachingWorker).to receive(:perform_async) allow(ReactiveCachingWorker).to receive(:perform_in) - write_reactive_cache(subject, data) if data + write_reactive_cache(subject, data, *qualifiers) if data end - def read_reactive_cache(subject) - Rails.cache.read(reactive_cache_key(subject)) + def read_reactive_cache(subject, *qualifiers) + Rails.cache.read(reactive_cache_key(subject, *qualifiers)) end - def write_reactive_cache(subject, data) - start_reactive_cache_lifetime(subject) - Rails.cache.write(reactive_cache_key(subject), data) + def write_reactive_cache(subject, data, *qualifiers) + start_reactive_cache_lifetime(subject, *qualifiers) + Rails.cache.write(reactive_cache_key(subject, *qualifiers), data) end - def reactive_cache_alive?(subject) - Rails.cache.read(reactive_cache_key(subject, 'alive')) + def reactive_cache_alive?(subject, *qualifiers) + Rails.cache.read(alive_reactive_cache_key(subject, *qualifiers)) end - def invalidate_reactive_cache(subject) - Rails.cache.delete(reactive_cache_key(subject, 'alive')) + def invalidate_reactive_cache(subject, *qualifiers) + Rails.cache.delete(alive_reactive_cache_key(subject, *qualifiers)) end - def start_reactive_cache_lifetime(subject) - Rails.cache.write(reactive_cache_key(subject, 'alive'), true) + def start_reactive_cache_lifetime(subject, *qualifiers) + Rails.cache.write(alive_reactive_cache_key(subject, *qualifiers), true) end def expect_reactive_cache_update_queued(subject) -- cgit v1.2.1 From 5c253116ae57c9db567394f544a40997de115f82 Mon Sep 17 00:00:00 2001 From: Panagiotis Atmatzidis Date: Wed, 12 Oct 2016 15:08:57 +0300 Subject: Adding 'IssueFinder' for 'projects' API endpoint Current filtering for "/projects/:id/issues" endpoint returns no results for "No Milestone" query. This PR introduces fix by copying filtering from "/groups/:id/issues" which works as expected. --- lib/api/issues.rb | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 54b97402426..dc55bb10636 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -88,12 +88,14 @@ module API use :issues_params end get ":id/issues" do - issues = IssuesFinder.new(current_user, - project_id: user_project.id, - state: params[:state], - milestone_title: params[:milestone]).execute.inc_notes_with_associations - issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil? - issues = filter_by_iid(issues, params[:iid]) unless params[:iid].nil? + project = find_project(params[:id]) + params[:state] ||= 'opened' + params[:project_id] = project.id + params[:milestone_title] = params.delete(:milestone) + params[:label_name] = params.delete(:labels) + + issues = IssuesFinder.new(current_user, params).execute + issues = issues.reorder(params[:order_by] => params[:sort]) present paginate(issues), with: Entities::Issue, current_user: current_user, project: user_project -- cgit v1.2.1 From 7ef1c6408e95c780e947e5c56cfae10b3dbb03d4 Mon Sep 17 00:00:00 2001 From: David Eisner Date: Wed, 26 Oct 2016 09:08:58 +0100 Subject: API: Implement project issues iid param with IssuesFinder and add tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use IssuesFinder for the /issues API resouce - Tests for iid filter in project issues API resource - Tests for No Milestone filter in issues API resources The "No Milestone" case was not previously tested, and the `/issues` resource did not support the the `milestone` parameter. - Return issues where all labels match from the issues and project issues API resources, like the group issues resource already does. See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6825#note_17474533 Signed-off-by: Rémy Coutable --- ...ilestone-option-for-projects-endpoint-23194.yml | 4 ++ doc/api/issues.md | 3 + lib/api/issues.rb | 47 +++++++------ spec/requests/api/issues_spec.rb | 80 +++++++++++++++++++++- 4 files changed, 110 insertions(+), 24 deletions(-) create mode 100644 changelogs/unreleased/fix-no-milestone-option-for-projects-endpoint-23194.yml diff --git a/changelogs/unreleased/fix-no-milestone-option-for-projects-endpoint-23194.yml b/changelogs/unreleased/fix-no-milestone-option-for-projects-endpoint-23194.yml new file mode 100644 index 00000000000..98066537723 --- /dev/null +++ b/changelogs/unreleased/fix-no-milestone-option-for-projects-endpoint-23194.yml @@ -0,0 +1,4 @@ +--- +title: 'API: fix query response for `/projects/:id/issues?milestone="No%20Milestone"`' +merge_request: 8457 +author: Panagiotis Atmatzidis, David Eisner diff --git a/doc/api/issues.md b/doc/api/issues.md index 119125bcd3d..dd84afd7c73 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -23,12 +23,15 @@ GET /issues?state=closed GET /issues?labels=foo GET /issues?labels=foo,bar GET /issues?labels=foo,bar&state=opened +GET /issues?milestone=1.0.0 +GET /issues?milestone=1.0.0&state=opened ``` | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `state` | string | no | Return all issues or just those that are `opened` or `closed`| | `labels` | string | no | Comma-separated list of label names, issues with any of the labels will be returned | +| `milestone` | string| no | The milestone title | | `order_by`| string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` | | `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` | diff --git a/lib/api/issues.rb b/lib/api/issues.rb index dc55bb10636..161269cbd41 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -5,13 +5,31 @@ module API before { authenticate! } helpers do - # TODO: Remove in 9.0 and switch to IssueFinder-based label filtering - def filter_issues_labels(issues, labels) - issues.includes(:labels).where('labels.title' => labels.split(',')) + def find_issues(args = {}) + args = params.merge(args) + + args.delete(:id) + args[:milestone_title] = args.delete(:milestone) + + match_all_labels = args.delete(:match_all_labels) + labels = args.delete(:labels) + args[:label_name] = labels if match_all_labels + + args[:search] = "#{Issue.reference_prefix}#{args.delete(:iid)}" if args.key?(:iid) + + issues = IssuesFinder.new(current_user, args).execute.inc_notes_with_associations + + # TODO: Remove in 9.0 pass `label_name: args.delete(:labels)` to IssuesFinder + if !match_all_labels && labels.present? + issues = issues.includes(:labels).where('labels.title' => labels.split(',')) + end + + issues.reorder(args[:order_by] => args[:sort]) end params :issues_params do optional :labels, type: String, desc: 'Comma-separated list of label names' + optional :milestone, type: String, desc: 'Milestone title' optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at', desc: 'Return issues ordered by `created_at` or `updated_at` fields.' optional :sort, type: String, values: %w[asc desc], default: 'desc', @@ -40,9 +58,7 @@ module API use :issues_params end get do - issues = IssuesFinder.new(current_user, scope: 'all', author_id: current_user.id, state: params[:state]).execute.inc_notes_with_associations - issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil? - issues = issues.reorder(params[:order_by] => params[:sort]) + issues = find_issues(scope: 'authored') present paginate(issues), with: Entities::Issue, current_user: current_user end @@ -61,15 +77,10 @@ module API use :issues_params end get ":id/issues" do - group = find_group!(params.delete(:id)) + group = find_group!(params[:id]) - params[:group_id] = group.id - params[:milestone_title] = params.delete(:milestone) - params[:label_name] = params.delete(:labels) + issues = find_issues(group_id: group.id, state: params[:state] || 'opened', match_all_labels: true) - issues = IssuesFinder.new(current_user, params).execute - - issues = issues.reorder(params[:order_by] => params[:sort]) present paginate(issues), with: Entities::Issue, current_user: current_user end end @@ -84,19 +95,13 @@ module API params do optional :state, type: String, values: %w[opened closed all], default: 'all', desc: 'Return opened, closed, or all issues' - optional :iid, type: Integer, desc: 'The IID of the issue' + optional :iid, type: Integer, desc: 'Return the issue having the given `iid`' use :issues_params end get ":id/issues" do project = find_project(params[:id]) - params[:state] ||= 'opened' - params[:project_id] = project.id - params[:milestone_title] = params.delete(:milestone) - params[:label_name] = params.delete(:labels) - - issues = IssuesFinder.new(current_user, params).execute - issues = issues.reorder(params[:order_by] => params[:sort]) + issues = find_issues(project_id: project.id) present paginate(issues), with: Entities::Issue, current_user: current_user, project: user_project end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index a786dc9edb3..432ba8f0f26 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -107,6 +107,7 @@ describe API::Issues, api: true do it 'returns an array of labeled issues when at least one label matches' do get api("/issues?labels=#{label.title},foo,bar", user) + expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(1) @@ -136,6 +137,51 @@ describe API::Issues, api: true do expect(json_response.length).to eq(0) end + it 'returns an empty array if no issue matches milestone' do + get api("/issues?milestone=#{empty_milestone.title}", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + + it 'returns an empty array if milestone does not exist' do + get api("/issues?milestone=foo", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + + it 'returns an array of issues in given milestone' do + get api("/issues?milestone=#{milestone.title}", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.first['id']).to eq(issue.id) + expect(json_response.second['id']).to eq(closed_issue.id) + end + + it 'returns an array of issues matching state in milestone' do + get api("/issues?milestone=#{milestone.title}"\ + '&state=closed', user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(closed_issue.id) + end + + it 'returns an array of issues with no milestone' do + get api("/issues?milestone=#{Milestone::None.title}", author) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(confidential_issue.id) + end + it 'sorts by created_at descending by default' do get api('/issues', user) response_dates = json_response.map { |issue| issue['created_at'] } @@ -318,6 +364,15 @@ describe API::Issues, api: true do expect(json_response.first['id']).to eq(group_closed_issue.id) end + it 'returns an array of issues with no milestone' do + get api("#{base_url}?milestone=#{Milestone::None.title}", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(group_confidential_issue.id) + end + it 'sorts by created_at descending by default' do get api(base_url, user) response_dates = json_response.map { |issue| issue['created_at'] } @@ -357,7 +412,6 @@ describe API::Issues, api: true do describe "GET /projects/:id/issues" do let(:base_url) { "/projects/#{project.id}" } - let(:title) { milestone.title } it "returns 404 on private projects for other users" do private_project = create(:empty_project, :private) @@ -433,8 +487,9 @@ describe API::Issues, api: true do expect(json_response.first['labels']).to eq([label.title]) end - it 'returns an array of labeled project issues when at least one label matches' do + it 'returns an array of labeled project issues where all labels match' do get api("#{base_url}/issues?labels=#{label.title},foo,bar", user) + expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(1) @@ -463,7 +518,8 @@ describe API::Issues, api: true do end it 'returns an array of issues in given milestone' do - get api("#{base_url}/issues?milestone=#{title}", user) + get api("#{base_url}/issues?milestone=#{milestone.title}", user) + expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(2) @@ -480,6 +536,15 @@ describe API::Issues, api: true do expect(json_response.first['id']).to eq(closed_issue.id) end + it 'returns an array of issues with no milestone' do + get api("#{base_url}/issues?milestone=#{Milestone::None.title}", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(confidential_issue.id) + end + it 'sorts by created_at descending by default' do get api("#{base_url}/issues", user) response_dates = json_response.map { |issue| issue['created_at'] } @@ -547,12 +612,21 @@ describe API::Issues, api: true do it 'returns a project issue by iid' do get api("/projects/#{project.id}/issues?iid=#{issue.iid}", user) + expect(response.status).to eq 200 + expect(json_response.length).to eq 1 expect(json_response.first['title']).to eq issue.title expect(json_response.first['id']).to eq issue.id expect(json_response.first['iid']).to eq issue.iid end + it 'returns an empty array for an unknown project issue iid' do + get api("/projects/#{project.id}/issues?iid=#{issue.iid + 10}", user) + + expect(response.status).to eq 200 + expect(json_response.length).to eq 0 + end + it "returns 404 if issue id not found" do get api("/projects/#{project.id}/issues/54321", user) expect(response).to have_http_status(404) -- cgit v1.2.1 From e1521b748bc12f71fa7b8f311fcefb9bd3aca8ae Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Thu, 12 Jan 2017 18:27:06 -0500 Subject: Mutate the attribute instead of issuing a write operation to the DB This fixes gitlab-org/gitlab-ee#1520 --- app/models/concerns/project_features_compatibility.rb | 2 +- changelogs/unreleased/bug-project-feature-compatibility.yml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/bug-project-feature-compatibility.yml diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb index 6d88951c713..60734bc6660 100644 --- a/app/models/concerns/project_features_compatibility.rb +++ b/app/models/concerns/project_features_compatibility.rb @@ -32,6 +32,6 @@ module ProjectFeaturesCompatibility build_project_feature unless project_feature access_level = Gitlab::Utils.to_boolean(value) ? ProjectFeature::ENABLED : ProjectFeature::DISABLED - project_feature.update_attribute(field, access_level) + project_feature.send(:write_attribute, field, access_level) end end diff --git a/changelogs/unreleased/bug-project-feature-compatibility.yml b/changelogs/unreleased/bug-project-feature-compatibility.yml new file mode 100644 index 00000000000..2124ee085e0 --- /dev/null +++ b/changelogs/unreleased/bug-project-feature-compatibility.yml @@ -0,0 +1,5 @@ +--- +title: Mutate the attribute instead of issuing a write operation to the DB in `ProjectFeaturesCompatibility` + concern. +merge_request: 8552 +author: -- cgit v1.2.1 From 96cec4fedec2a3761737bf008fb097de79663d1e Mon Sep 17 00:00:00 2001 From: victorwu Date: Wed, 11 Jan 2017 15:40:51 -0500 Subject: Text for copy to clipboard: URL, personal access token, commands, reference --- app/views/profiles/personal_access_tokens/index.html.haml | 2 +- app/views/projects/merge_requests/show/_how_to_merge.html.haml | 6 +++--- app/views/shared/_clone_panel.html.haml | 2 +- app/views/shared/issuable/_sidebar.html.haml | 4 ++-- changelogs/unreleased/clipboard-button-text.yml | 3 +++ 5 files changed, 10 insertions(+), 7 deletions(-) create mode 100644 changelogs/unreleased/clipboard-button-text.yml diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml index bb4effeeeb1..60a561c9f9c 100644 --- a/app/views/profiles/personal_access_tokens/index.html.haml +++ b/app/views/profiles/personal_access_tokens/index.html.haml @@ -19,7 +19,7 @@ Your New Personal Access Token .form-group = text_field_tag 'created-personal-access-token', flash[:personal_access_token], readonly: true, class: "form-control", 'aria-describedby' => "created-personal-access-token-help-block" - = clipboard_button(clipboard_text: flash[:personal_access_token]) + = clipboard_button(clipboard_text: flash[:personal_access_token], title: "Copy personal access token to clipboard", placement: "left") %span#created-personal-access-token-help-block.help-block.text-danger Make sure you save it - you won't be able to access it again. %hr diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml index ec76c6a5417..93ed4b68e0e 100644 --- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml +++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml @@ -8,7 +8,7 @@ %p %strong Step 1. Fetch and check out the branch for this merge request - = clipboard_button(clipboard_target: "pre#merge-info-1") + = clipboard_button(clipboard_target: "pre#merge-info-1", title: "Copy commands to clipboard") %pre.dark#merge-info-1 - if @merge_request.for_fork? :preserve @@ -25,7 +25,7 @@ %p %strong Step 3. Merge the branch and fix any conflicts that come up - = clipboard_button(clipboard_target: "pre#merge-info-3") + = clipboard_button(clipboard_target: "pre#merge-info-3", title: "Copy commands to clipboard") %pre.dark#merge-info-3 - if @merge_request.for_fork? :preserve @@ -38,7 +38,7 @@ %p %strong Step 4. Push the result of the merge to GitLab - = clipboard_button(clipboard_target: "pre#merge-info-4") + = clipboard_button(clipboard_target: "pre#merge-info-4", title: "Copy commands to clipboard") %pre.dark#merge-info-4 :preserve git push origin #{h @merge_request.target_branch} diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index 96b75440309..03684389742 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -19,7 +19,7 @@ = text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true .input-group-btn - = clipboard_button(clipboard_target: '#project_clone') + = clipboard_button(clipboard_target: '#project_clone', title: "Copy URL to clipboard") :javascript $('ul.clone-options-dropdown a').on('click',function(e){ diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 5f199301364..a02b815e3cd 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -153,13 +153,13 @@ - project_ref = cross_project_reference(@project, issuable) .block.project-reference .sidebar-collapsed-icon.dont-change-state - = clipboard_button(clipboard_text: project_ref) + = clipboard_button(clipboard_text: project_ref, title: "Copy reference to clipboard", placement: "left") .cross-project-reference.hide-collapsed %span Reference: %cite{ title: project_ref } = project_ref - = clipboard_button(clipboard_text: project_ref) + = clipboard_button(clipboard_text: project_ref, title: "Copy reference to clipboard", placement: "left") :javascript new MilestoneSelect('{"namespace":"#{@project.namespace.path}","path":"#{@project.path}"}'); diff --git a/changelogs/unreleased/clipboard-button-text.yml b/changelogs/unreleased/clipboard-button-text.yml new file mode 100644 index 00000000000..dc93da60426 --- /dev/null +++ b/changelogs/unreleased/clipboard-button-text.yml @@ -0,0 +1,3 @@ +--- +title: 'Copy to clipboard' +merge_request: 8535 -- cgit v1.2.1 From 892ff3a3ae640272f8712fb190242f2b1fe010a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 12 Jan 2017 13:12:50 -0500 Subject: Check for env[Grape::Env::GRAPE_ROUTING_ARGS] instead of endpoint.route MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `endpoint.route` is calling `env[Grape::Env::GRAPE_ROUTING_ARGS][:route_info]` but `env[Grape::Env::GRAPE_ROUTING_ARGS]` is `nil` in the case of a 405 response Signed-off-by: Rémy Coutable --- .../26587-metrics-middleware-endpoint-is-nil.yml | 4 ++++ lib/gitlab/metrics/rack_middleware.rb | 15 +++++++++++---- spec/lib/gitlab/metrics/rack_middleware_spec.rb | 11 +++++++++++ 3 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/26587-metrics-middleware-endpoint-is-nil.yml diff --git a/changelogs/unreleased/26587-metrics-middleware-endpoint-is-nil.yml b/changelogs/unreleased/26587-metrics-middleware-endpoint-is-nil.yml new file mode 100644 index 00000000000..5891a5ef6e8 --- /dev/null +++ b/changelogs/unreleased/26587-metrics-middleware-endpoint-is-nil.yml @@ -0,0 +1,4 @@ +--- +title: Check for env[Grape::Env::GRAPE_ROUTING_ARGS] instead of endpoint.route +merge_request: 8544 +author: diff --git a/lib/gitlab/metrics/rack_middleware.rb b/lib/gitlab/metrics/rack_middleware.rb index d01d47a6a7a..47f88727fc8 100644 --- a/lib/gitlab/metrics/rack_middleware.rb +++ b/lib/gitlab/metrics/rack_middleware.rb @@ -71,10 +71,17 @@ module Gitlab def tag_endpoint(trans, env) endpoint = env[ENDPOINT_KEY] - # endpoint.route is nil in the case of a 405 response - if endpoint.route - path = endpoint_paths_cache[endpoint.route.request_method][endpoint.route.path] - trans.action = "Grape##{endpoint.route.request_method} #{path}" + begin + route = endpoint.route + rescue + # endpoint.route is calling env[Grape::Env::GRAPE_ROUTING_ARGS][:route_info] + # but env[Grape::Env::GRAPE_ROUTING_ARGS] is nil in the case of a 405 response + # so we're rescuing exceptions and bailing out + end + + if route + path = endpoint_paths_cache[route.request_method][route.path] + trans.action = "Grape##{route.request_method} #{path}" end end diff --git a/spec/lib/gitlab/metrics/rack_middleware_spec.rb b/spec/lib/gitlab/metrics/rack_middleware_spec.rb index 7371b578a48..fb470ea7568 100644 --- a/spec/lib/gitlab/metrics/rack_middleware_spec.rb +++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb @@ -126,5 +126,16 @@ describe Gitlab::Metrics::RackMiddleware do expect(transaction.action).to eq('Grape#GET /projects/:id/archive') end + + it 'does not tag a transaction if route infos are missing' do + endpoint = double(:endpoint) + allow(endpoint).to receive(:route).and_raise + + env['api.endpoint'] = endpoint + + middleware.tag_endpoint(transaction, env) + + expect(transaction.action).to be_nil + end end end -- cgit v1.2.1 From a532c6040c1c150810122b172a2fad30d1753dfd Mon Sep 17 00:00:00 2001 From: Semyon Pupkov Date: Wed, 14 Dec 2016 00:54:50 +0500 Subject: Allow to use ENV variables in redis config --- changelogs/unreleased/env-var-in-redis-config.yml | 4 ++++ doc/install/installation.md | 6 ++++++ lib/gitlab/redis.rb | 2 +- spec/fixtures/config/redis_config_with_env.yml | 2 ++ spec/lib/gitlab/redis_spec.rb | 16 +++++++++++++++- 5 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/env-var-in-redis-config.yml create mode 100644 spec/fixtures/config/redis_config_with_env.yml diff --git a/changelogs/unreleased/env-var-in-redis-config.yml b/changelogs/unreleased/env-var-in-redis-config.yml new file mode 100644 index 00000000000..561ea7f514e --- /dev/null +++ b/changelogs/unreleased/env-var-in-redis-config.yml @@ -0,0 +1,4 @@ +--- +title: Allow to use ENV variables in redis config +merge_request: 8073 +author: Semyon Pupkov diff --git a/doc/install/installation.md b/doc/install/installation.md index 9dba03b1924..9cebed34b7e 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -601,6 +601,12 @@ If you want to connect the Redis server via socket, then use the "unix:" URL sch production: url: unix:/path/to/redis/socket +Also you can use environment variables in the `config/resque.yml` file: + + # example + production: + url: <%= ENV.fetch('GITLAB_REDIS_URL') %> + ### Custom SSH Connection If you are running SSH on a non-standard port, you must change the GitLab user's SSH config. diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb index 9226da2d6b1..9384102acec 100644 --- a/lib/gitlab/redis.rb +++ b/lib/gitlab/redis.rb @@ -42,7 +42,7 @@ module Gitlab return @_raw_config if defined?(@_raw_config) begin - @_raw_config = File.read(CONFIG_FILE).freeze + @_raw_config = ERB.new(File.read(CONFIG_FILE)).result.freeze rescue Errno::ENOENT @_raw_config = false end diff --git a/spec/fixtures/config/redis_config_with_env.yml b/spec/fixtures/config/redis_config_with_env.yml new file mode 100644 index 00000000000..f5860f37e47 --- /dev/null +++ b/spec/fixtures/config/redis_config_with_env.yml @@ -0,0 +1,2 @@ +test: + url: <%= ENV['TEST_GITLAB_REDIS_URL'] %> diff --git a/spec/lib/gitlab/redis_spec.rb b/spec/lib/gitlab/redis_spec.rb index e5406fb2d33..917c5c46db1 100644 --- a/spec/lib/gitlab/redis_spec.rb +++ b/spec/lib/gitlab/redis_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::Redis do - let(:redis_config) { Rails.root.join('config', 'resque.yml').to_s } + include StubENV before(:each) { clear_raw_config } after(:each) { clear_raw_config } @@ -72,6 +72,20 @@ describe Gitlab::Redis do expect(url2).not_to end_with('foobar') end + + context 'when yml file with env variable' do + let(:redis_config) { Rails.root.join('spec/fixtures/config/redis_config_with_env.yml') } + + before do + stub_env('TEST_GITLAB_REDIS_URL', 'redis://redishost:6379') + end + + it 'reads redis url from env variable' do + stub_const("#{described_class}::CONFIG_FILE", redis_config) + + expect(described_class.url).to eq 'redis://redishost:6379' + end + end end describe '._raw_config' do -- cgit v1.2.1 From 0f84e747b0db1738caa04011faac4b9bd4250a21 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 13 Jan 2017 09:15:52 +0100 Subject: Add check for new path existence --- .../20161226122833_remove_dot_git_from_usernames.rb | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/db/migrate/20161226122833_remove_dot_git_from_usernames.rb b/db/migrate/20161226122833_remove_dot_git_from_usernames.rb index 809b09feb84..7d97339581f 100644 --- a/db/migrate/20161226122833_remove_dot_git_from_usernames.rb +++ b/db/migrate/20161226122833_remove_dot_git_from_usernames.rb @@ -14,9 +14,8 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration namespace_id = user['namespace_id'] path_was = user['username'] path_was_wildcard = quote_string("#{path_was}/%") - path = quote_string(rename_path(path_was)) - move_namespace(namespace_id, path_was, path) + path = move_namespace(namespace_id, path_was, path) execute "UPDATE routes SET path = '#{path}' WHERE source_type = 'Namespace' AND source_id = #{namespace_id}" execute "UPDATE namespaces SET path = '#{path}' WHERE id = #{namespace_id}" @@ -45,9 +44,13 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration select_all("SELECT id, path FROM routes WHERE path = '#{quote_string(path)}'").present? end + def path_exists?(repository_storage_path, path) + gitlab_shell.exists?(repository_storage_path, path) + end + # Accepts invalid path like test.git and returns test_git or # test_git1 if test_git already taken - def rename_path(path) + def rename_path(repository_storage_path, path) # To stay closer with original name and reduce risk of duplicates # we rename suffix instead of removing it path = path.sub(/\.git\z/, '_git') @@ -55,7 +58,7 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration counter = 0 base = path - while route_exists?(path) + while route_exists?(path) || path_exists?(repository_storage_path, path) counter += 1 path = "#{base}#{counter}" end @@ -73,6 +76,8 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration # Ensure old directory exists before moving it gitlab_shell.add_namespace(repository_storage_path, path_was) + path = quote_string(rename_path(repository_storage_path, path_was)) + unless gitlab_shell.mv_namespace(repository_storage_path, path_was, path) Rails.logger.error "Exception moving path #{repository_storage_path} from #{path_was} to #{path}" @@ -83,5 +88,7 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration end Gitlab::UploadsTransfer.new.rename_namespace(path_was, path) + + path end end -- cgit v1.2.1 From b0ae433ef361f62cd95c40035855c31d3dca784d Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Fri, 13 Jan 2017 10:02:46 +0000 Subject: Fix milestone API specs in Ruby 2.1 Ruby 2.3: ``` URI.parse('http://foo/bar?baz quux') => # ``` Ruby 2.1: ``` URI.parse('http://foo/bar?baz quux') URI::InvalidURIError: bad URI(is not URI?): http://foo/bar?baz quux ``` --- .gitlab-ci.yml | 5 ----- spec/support/api_helpers.rb | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 68690ff33da..2dced4a3351 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -166,11 +166,6 @@ spinach 9 10: *spinach-knapsack .ruby-21: &ruby-21 image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.1-git-2.7-phantomjs-2.1" <<: *use-db - only: - - master@gitlab-org/gitlab-ce - - master@gitlab-org/gitlab-ee - - master@gitlab/gitlabhq - - master@gitlab/gitlab-ee cache: key: "ruby21" paths: diff --git a/spec/support/api_helpers.rb b/spec/support/api_helpers.rb index 68b196d9033..e25cb9ecd89 100644 --- a/spec/support/api_helpers.rb +++ b/spec/support/api_helpers.rb @@ -18,7 +18,7 @@ module ApiHelpers # # Returns the relative path to the requested API resource def api(path, user = nil) - "/api/#{API::API.version}#{path}" + + "/api/#{API::API.version}#{URI.escape(path)}" + # Normalize query string (path.index('?') ? '' : '?') + -- cgit v1.2.1 From 52afe463802ce37e6f551b05b38f70148171689a Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Fri, 13 Jan 2017 10:07:56 +0000 Subject: Only escape the 'No Milestone' param This is less invasive than the parent commit. --- spec/requests/api/issues_spec.rb | 8 +++++--- spec/support/api_helpers.rb | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 432ba8f0f26..12dd4bd83f7 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -50,6 +50,8 @@ describe API::Issues, api: true do end let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) } + let(:no_milestone_title) { URI.escape(Milestone::None.title) } + before do project.team << [user, :reporter] project.team << [guest, :guest] @@ -174,7 +176,7 @@ describe API::Issues, api: true do end it 'returns an array of issues with no milestone' do - get api("/issues?milestone=#{Milestone::None.title}", author) + get api("/issues?milestone=#{no_milestone_title}", author) expect(response).to have_http_status(200) expect(json_response).to be_an Array @@ -365,7 +367,7 @@ describe API::Issues, api: true do end it 'returns an array of issues with no milestone' do - get api("#{base_url}?milestone=#{Milestone::None.title}", user) + get api("#{base_url}?milestone=#{no_milestone_title}", user) expect(response).to have_http_status(200) expect(json_response).to be_an Array @@ -537,7 +539,7 @@ describe API::Issues, api: true do end it 'returns an array of issues with no milestone' do - get api("#{base_url}/issues?milestone=#{Milestone::None.title}", user) + get api("#{base_url}/issues?milestone=#{no_milestone_title}", user) expect(response).to have_http_status(200) expect(json_response).to be_an Array diff --git a/spec/support/api_helpers.rb b/spec/support/api_helpers.rb index e25cb9ecd89..68b196d9033 100644 --- a/spec/support/api_helpers.rb +++ b/spec/support/api_helpers.rb @@ -18,7 +18,7 @@ module ApiHelpers # # Returns the relative path to the requested API resource def api(path, user = nil) - "/api/#{API::API.version}#{URI.escape(path)}" + + "/api/#{API::API.version}#{path}" + # Normalize query string (path.index('?') ? '' : '?') + -- cgit v1.2.1 From 5639c1a338ee5a9c02f8fcc2b818d74541d4d6fa Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Fri, 13 Jan 2017 11:45:48 +0000 Subject: Put .gitlab-ci.yml back to only 2.1 on master [ci skip] --- .gitlab-ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2dced4a3351..68690ff33da 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -166,6 +166,11 @@ spinach 9 10: *spinach-knapsack .ruby-21: &ruby-21 image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.1-git-2.7-phantomjs-2.1" <<: *use-db + only: + - master@gitlab-org/gitlab-ce + - master@gitlab-org/gitlab-ee + - master@gitlab/gitlabhq + - master@gitlab/gitlab-ee cache: key: "ruby21" paths: -- cgit v1.2.1 From 1f546e4d724d77db0a384fb36e17243bf75381f5 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 13 Jan 2017 13:07:32 +0100 Subject: Update commit entity to point to valid commit page --- app/serializers/commit_entity.rb | 8 ++++---- spec/serializers/commit_entity_spec.rb | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/serializers/commit_entity.rb b/app/serializers/commit_entity.rb index 49f4db36295..31763955f97 100644 --- a/app/serializers/commit_entity.rb +++ b/app/serializers/commit_entity.rb @@ -8,16 +8,16 @@ class CommitEntity < API::Entities::RepoCommit end expose :commit_url do |commit| - namespace_project_tree_url( + namespace_project_commit_url( request.project.namespace, request.project, - id: commit.id) + commit) end expose :commit_path do |commit| - namespace_project_tree_path( + namespace_project_commit_path( request.project.namespace, request.project, - id: commit.id) + commit) end end diff --git a/spec/serializers/commit_entity_spec.rb b/spec/serializers/commit_entity_spec.rb index a8662e81d20..0333d73b5b5 100644 --- a/spec/serializers/commit_entity_spec.rb +++ b/spec/serializers/commit_entity_spec.rb @@ -33,10 +33,12 @@ describe CommitEntity do it 'contains path to commit' do expect(subject).to include(:commit_path) + expect(subject[:commit_path]).to include "commit/#{commit.id}" end it 'contains URL to commit' do expect(subject).to include(:commit_url) + expect(subject[:commit_path]).to include "commit/#{commit.id}" end it 'needs to receive project in the request' do -- cgit v1.2.1 From 46a5a8e2b1a86452bc65e72eae6a4322a38d2930 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 13 Jan 2017 09:27:16 -0500 Subject: Fixed spacing of labels in issuable row on milestone#show --- app/assets/stylesheets/pages/milestone.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index e284b7269ce..686b64cdd24 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -109,6 +109,10 @@ .avatar { float: none; } + + > a:not(:last-of-type) { + margin-right: 5px; + } } } -- cgit v1.2.1 From 0433790958e3ce701dd46f697f421f2db69fba10 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 13 Jan 2017 15:34:14 +0100 Subject: Merge build specs into file that has valid location --- spec/models/build_spec.rb | 1332 ------------------------------------------ spec/models/ci/build_spec.rb | 1279 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 1276 insertions(+), 1335 deletions(-) diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index cd3b6d51545..64c751e791f 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -1,1338 +1,6 @@ require 'spec_helper' describe Ci::Build, models: true do - let(:project) { create(:project) } - let(:pipeline) do - create(:ci_pipeline, project: project, - sha: project.commit.id, - ref: project.default_branch, - status: 'success') - end - let(:build) { create(:ci_build, pipeline: pipeline) } - - it { is_expected.to validate_presence_of :ref } - - it { is_expected.to respond_to :trace_html } - - describe '#first_pending' do - let!(:first) { create(:ci_build, pipeline: pipeline, status: 'pending', created_at: Date.yesterday) } - let!(:second) { create(:ci_build, pipeline: pipeline, status: 'pending') } - subject { Ci::Build.first_pending } - - it { is_expected.to be_a(Ci::Build) } - it('returns with the first pending build') { is_expected.to eq(first) } - end - - describe '#create_from' do - before do - build.status = 'success' - build.save - end - let(:create_from_build) { Ci::Build.create_from build } - - it 'exists a pending task' do - expect(Ci::Build.pending.count(:all)).to eq 0 - create_from_build - expect(Ci::Build.pending.count(:all)).to be > 0 - end - end - - describe '#failed_but_allowed?' do - subject { build.failed_but_allowed? } - - context 'when build is not allowed to fail' do - before do - build.allow_failure = false - end - - context 'and build.status is success' do - before do - build.status = 'success' - end - - it { is_expected.to be_falsey } - end - - context 'and build.status is failed' do - before do - build.status = 'failed' - end - - it { is_expected.to be_falsey } - end - end - - context 'when build is allowed to fail' do - before do - build.allow_failure = true - end - - context 'and build.status is success' do - before do - build.status = 'success' - end - - it { is_expected.to be_falsey } - end - - context 'and build.status is failed' do - before do - build.status = 'failed' - end - - it { is_expected.to be_truthy } - end - end - end - - describe '#persisted_environment' do - before do - @environment = create(:environment, project: project, name: "foo-#{project.default_branch}") - end - - subject { build.persisted_environment } - - context 'referenced literally' do - let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-#{project.default_branch}") } - - it { is_expected.to eq(@environment) } - end - - context 'referenced with a variable' do - let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-$CI_BUILD_REF_NAME") } - - it { is_expected.to eq(@environment) } - end - end - - describe '#trace' do - it { expect(build.trace).to be_nil } - - context 'when build.trace contains text' do - let(:text) { 'example output' } - before do - build.trace = text - end - - it { expect(build.trace).to eq(text) } - end - - context 'when build.trace hides runners token' do - let(:token) { 'my_secret_token' } - - before do - build.update(trace: token) - build.project.update(runners_token: token) - end - - it { expect(build.trace).not_to include(token) } - it { expect(build.raw_trace).to include(token) } - end - - context 'when build.trace hides build token' do - let(:token) { 'my_secret_token' } - - before do - build.update(trace: token) - build.update(token: token) - end - - it { expect(build.trace).not_to include(token) } - it { expect(build.raw_trace).to include(token) } - end - end - - describe '#raw_trace' do - subject { build.raw_trace } - - context 'when build.trace hides runners token' do - let(:token) { 'my_secret_token' } - - before do - build.project.update(runners_token: token) - build.update(trace: token) - end - - it { is_expected.not_to include(token) } - end - - context 'when build.trace hides build token' do - let(:token) { 'my_secret_token' } - - before do - build.update(token: token) - build.update(trace: token) - end - - it { is_expected.not_to include(token) } - end - end - - context '#append_trace' do - subject { build.trace_html } - - context 'when build.trace hides runners token' do - let(:token) { 'my_secret_token' } - - before do - build.project.update(runners_token: token) - build.append_trace(token, 0) - end - - it { is_expected.not_to include(token) } - end - - context 'when build.trace hides build token' do - let(:token) { 'my_secret_token' } - - before do - build.update(token: token) - build.append_trace(token, 0) - end - - it { is_expected.not_to include(token) } - end - end - - # TODO: build timeout - # describe :timeout do - # subject { build.timeout } - # - # it { is_expected.to eq(pipeline.project.timeout) } - # end - - describe '#options' do - let(:options) do - { - image: "ruby:2.1", - services: [ - "postgres" - ] - } - end - - subject { build.options } - it { is_expected.to eq(options) } - end - - # TODO: allow_git_fetch - # describe :allow_git_fetch do - # subject { build.allow_git_fetch } - # - # it { is_expected.to eq(project.allow_git_fetch) } - # end - - describe '#project' do - subject { build.project } - - it { is_expected.to eq(pipeline.project) } - end - - describe '#project_id' do - subject { build.project_id } - - it { is_expected.to eq(pipeline.project_id) } - end - - describe '#project_name' do - subject { build.project_name } - - it { is_expected.to eq(project.name) } - end - - describe '#extract_coverage' do - context 'valid content & regex' do - subject { build.extract_coverage('Coverage 1033 / 1051 LOC (98.29%) covered', '\(\d+.\d+\%\) covered') } - - it { is_expected.to eq(98.29) } - end - - context 'valid content & bad regex' do - subject { build.extract_coverage('Coverage 1033 / 1051 LOC (98.29%) covered', 'very covered') } - - it { is_expected.to be_nil } - end - - context 'no coverage content & regex' do - subject { build.extract_coverage('No coverage for today :sad:', '\(\d+.\d+\%\) covered') } - - it { is_expected.to be_nil } - end - - context 'multiple results in content & regex' do - subject { build.extract_coverage(' (98.39%) covered. (98.29%) covered', '\(\d+.\d+\%\) covered') } - - it { is_expected.to eq(98.29) } - end - - context 'using a regex capture' do - subject { build.extract_coverage('TOTAL 9926 3489 65%', 'TOTAL\s+\d+\s+\d+\s+(\d{1,3}\%)') } - - it { is_expected.to eq(65) } - end - end - - describe '#ref_slug' do - { - 'master' => 'master', - '1-foo' => '1-foo', - 'fix/1-foo' => 'fix-1-foo', - 'fix-1-foo' => 'fix-1-foo', - 'a' * 63 => 'a' * 63, - 'a' * 64 => 'a' * 63, - 'FOO' => 'foo', - }.each do |ref, slug| - it "transforms #{ref} to #{slug}" do - build.ref = ref - - expect(build.ref_slug).to eq(slug) - end - end - end - - describe '#variables' do - let(:container_registry_enabled) { false } - let(:predefined_variables) do - [ - { key: 'CI', value: 'true', public: true }, - { key: 'GITLAB_CI', value: 'true', public: true }, - { key: 'CI_BUILD_ID', value: build.id.to_s, public: true }, - { key: 'CI_BUILD_TOKEN', value: build.token, public: false }, - { key: 'CI_BUILD_REF', value: build.sha, public: true }, - { key: 'CI_BUILD_BEFORE_SHA', value: build.before_sha, public: true }, - { key: 'CI_BUILD_REF_NAME', value: 'master', public: true }, - { key: 'CI_BUILD_REF_SLUG', value: 'master', public: true }, - { key: 'CI_BUILD_NAME', value: 'test', public: true }, - { key: 'CI_BUILD_STAGE', value: 'test', public: true }, - { key: 'CI_SERVER_NAME', value: 'GitLab', public: true }, - { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true }, - { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true }, - { key: 'CI_PROJECT_ID', value: project.id.to_s, public: true }, - { key: 'CI_PROJECT_NAME', value: project.path, public: true }, - { key: 'CI_PROJECT_PATH', value: project.path_with_namespace, public: true }, - { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.path, public: true }, - { key: 'CI_PROJECT_URL', value: project.web_url, public: true }, - { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true } - ] - end - - before do - stub_container_registry_config(enabled: container_registry_enabled, host_port: 'registry.example.com') - end - - subject { build.variables } - - context 'returns variables' do - before do - build.yaml_variables = [] - end - - it { is_expected.to eq(predefined_variables) } - end - - context 'when build has user' do - let(:user) { create(:user, username: 'starter') } - let(:user_variables) do - [ - { key: 'GITLAB_USER_ID', value: user.id.to_s, public: true }, - { key: 'GITLAB_USER_EMAIL', value: user.email, public: true } - ] - end - - before do - build.update_attributes(user: user) - end - - it { user_variables.each { |v| is_expected.to include(v) } } - end - - context 'when build has an environment' do - before do - build.update(environment: 'production') - create(:environment, project: build.project, name: 'production', slug: 'prod-slug') - end - - let(:environment_variables) do - [ - { key: 'CI_ENVIRONMENT_NAME', value: 'production', public: true }, - { key: 'CI_ENVIRONMENT_SLUG', value: 'prod-slug', public: true } - ] - end - - it { environment_variables.each { |v| is_expected.to include(v) } } - end - - context 'when build started manually' do - before do - build.update_attributes(when: :manual) - end - - let(:manual_variable) do - { key: 'CI_BUILD_MANUAL', value: 'true', public: true } - end - - it { is_expected.to include(manual_variable) } - end - - context 'when build is for tag' do - let(:tag_variable) do - { key: 'CI_BUILD_TAG', value: 'master', public: true } - end - - before do - build.update_attributes(tag: true) - end - - it { is_expected.to include(tag_variable) } - end - - context 'when secure variable is defined' do - let(:secure_variable) do - { key: 'SECRET_KEY', value: 'secret_value', public: false } - end - - before do - build.project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value') - end - - it { is_expected.to include(secure_variable) } - end - - context 'when build is for triggers' do - let(:trigger) { create(:ci_trigger, project: project) } - let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger) } - let(:user_trigger_variable) do - { key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1', public: false } - end - let(:predefined_trigger_variable) do - { key: 'CI_BUILD_TRIGGERED', value: 'true', public: true } - end - - before do - build.trigger_request = trigger_request - end - - it { is_expected.to include(user_trigger_variable) } - it { is_expected.to include(predefined_trigger_variable) } - end - - context 'when yaml_variables are undefined' do - before do - build.yaml_variables = nil - end - - context 'use from gitlab-ci.yml' do - before do - stub_ci_pipeline_yaml_file(config) - end - - context 'when config is not found' do - let(:config) { nil } - - it { is_expected.to eq(predefined_variables) } - end - - context 'when config does not have a questioned job' do - let(:config) do - YAML.dump({ - test_other: { - script: 'Hello World' - } - }) - end - - it { is_expected.to eq(predefined_variables) } - end - - context 'when config has variables' do - let(:config) do - YAML.dump({ - test: { - script: 'Hello World', - variables: { - KEY: 'value' - } - } - }) - end - let(:variables) do - [{ key: 'KEY', value: 'value', public: true }] - end - - it { is_expected.to eq(predefined_variables + variables) } - end - end - end - - context 'when container registry is enabled' do - let(:container_registry_enabled) { true } - let(:ci_registry) do - { key: 'CI_REGISTRY', value: 'registry.example.com', public: true } - end - let(:ci_registry_image) do - { key: 'CI_REGISTRY_IMAGE', value: project.container_registry_repository_url, public: true } - end - - context 'and is disabled for project' do - before do - project.update(container_registry_enabled: false) - end - - it { is_expected.to include(ci_registry) } - it { is_expected.not_to include(ci_registry_image) } - end - - context 'and is enabled for project' do - before do - project.update(container_registry_enabled: true) - end - - it { is_expected.to include(ci_registry) } - it { is_expected.to include(ci_registry_image) } - end - end - - context 'when runner is assigned to build' do - let(:runner) { create(:ci_runner, description: 'description', tag_list: ['docker', 'linux']) } - - before do - build.update(runner: runner) - end - - it { is_expected.to include({ key: 'CI_RUNNER_ID', value: runner.id.to_s, public: true }) } - it { is_expected.to include({ key: 'CI_RUNNER_DESCRIPTION', value: 'description', public: true }) } - it { is_expected.to include({ key: 'CI_RUNNER_TAGS', value: 'docker, linux', public: true }) } - end - - context 'when build is for a deployment' do - let(:deployment_variable) { { key: 'KUBERNETES_TOKEN', value: 'TOKEN', public: false } } - - before do - build.environment = 'production' - allow(project).to receive(:deployment_variables).and_return([deployment_variable]) - end - - it { is_expected.to include(deployment_variable) } - end - - context 'returns variables in valid order' do - before do - allow(build).to receive(:predefined_variables) { ['predefined'] } - allow(project).to receive(:predefined_variables) { ['project'] } - allow(pipeline).to receive(:predefined_variables) { ['pipeline'] } - allow(build).to receive(:yaml_variables) { ['yaml'] } - allow(project).to receive(:secret_variables) { ['secret'] } - end - - it { is_expected.to eq(%w[predefined project pipeline yaml secret]) } - end - end - - describe '#has_tags?' do - context 'when build has tags' do - subject { create(:ci_build, tag_list: ['tag']) } - it { is_expected.to have_tags } - end - - context 'when build does not have tags' do - subject { create(:ci_build, tag_list: []) } - it { is_expected.not_to have_tags } - end - end - - describe '#any_runners_online?' do - subject { build.any_runners_online? } - - context 'when no runners' do - it { is_expected.to be_falsey } - end - - context 'when there are runners' do - let(:runner) { create(:ci_runner) } - - before do - build.project.runners << runner - runner.update_attributes(contacted_at: 1.second.ago) - end - - it { is_expected.to be_truthy } - - it 'that is inactive' do - runner.update_attributes(active: false) - is_expected.to be_falsey - end - - it 'that is not online' do - runner.update_attributes(contacted_at: nil) - is_expected.to be_falsey - end - - it 'that cannot handle build' do - expect_any_instance_of(Ci::Runner).to receive(:can_pick?).and_return(false) - is_expected.to be_falsey - end - end - end - - describe '#stuck?' do - subject { build.stuck? } - - context "when commit_status.status is pending" do - before do - build.status = 'pending' - end - - it { is_expected.to be_truthy } - - context "and there are specific runner" do - let(:runner) { create(:ci_runner, contacted_at: 1.second.ago) } - - before do - build.project.runners << runner - runner.save - end - - it { is_expected.to be_falsey } - end - end - - %w[success failed canceled running].each do |state| - context "when commit_status.status is #{state}" do - before do - build.status = state - end - - it { is_expected.to be_falsey } - end - end - end - - describe '#artifacts?' do - subject { build.artifacts? } - - context 'artifacts archive does not exist' do - before do - build.update_attributes(artifacts_file: nil) - end - - it { is_expected.to be_falsy } - end - - context 'artifacts archive exists' do - let(:build) { create(:ci_build, :artifacts) } - it { is_expected.to be_truthy } - - context 'is expired' do - before { build.update(artifacts_expire_at: Time.now - 7.days) } - it { is_expected.to be_falsy } - end - - context 'is not expired' do - before { build.update(artifacts_expire_at: Time.now + 7.days) } - it { is_expected.to be_truthy } - end - end - end - - describe '#artifacts_expired?' do - subject { build.artifacts_expired? } - - context 'is expired' do - before { build.update(artifacts_expire_at: Time.now - 7.days) } - - it { is_expected.to be_truthy } - end - - context 'is not expired' do - before { build.update(artifacts_expire_at: Time.now + 7.days) } - - it { is_expected.to be_falsey } - end - end - - describe '#artifacts_metadata?' do - subject { build.artifacts_metadata? } - context 'artifacts metadata does not exist' do - it { is_expected.to be_falsy } - end - - context 'artifacts archive is a zip file and metadata exists' do - let(:build) { create(:ci_build, :artifacts) } - it { is_expected.to be_truthy } - end - end - describe '#repo_url' do - let(:build) { create(:ci_build) } - let(:project) { build.project } - - subject { build.repo_url } - - it { is_expected.to be_a(String) } - it { is_expected.to end_with(".git") } - it { is_expected.to start_with(project.web_url[0..6]) } - it { is_expected.to include(build.token) } - it { is_expected.to include('gitlab-ci-token') } - it { is_expected.to include(project.web_url[7..-1]) } - end - - describe '#artifacts_expire_in' do - subject { build.artifacts_expire_in } - it { is_expected.to be_nil } - - context 'when artifacts_expire_at is specified' do - let(:expire_at) { Time.now + 7.days } - - before { build.artifacts_expire_at = expire_at } - - it { is_expected.to be_within(5).of(expire_at - Time.now) } - end - end - - describe '#artifacts_expire_in=' do - subject { build.artifacts_expire_in } - - it 'when assigning valid duration' do - build.artifacts_expire_in = '7 days' - - is_expected.to be_within(10).of(7.days.to_i) - end - - it 'when assigning invalid duration' do - expect { build.artifacts_expire_in = '7 elephants' }.to raise_error(ChronicDuration::DurationParseError) - is_expected.to be_nil - end - - it 'when resseting value' do - build.artifacts_expire_in = nil - - is_expected.to be_nil - end - end - - describe '#keep_artifacts!' do - let(:build) { create(:ci_build, artifacts_expire_at: Time.now + 7.days) } - - it 'to reset expire_at' do - build.keep_artifacts! - - expect(build.artifacts_expire_at).to be_nil - end - end - - describe '#depends_on_builds' do - let!(:build) { create(:ci_build, pipeline: pipeline, name: 'build', stage_idx: 0, stage: 'build') } - let!(:rspec_test) { create(:ci_build, pipeline: pipeline, name: 'rspec', stage_idx: 1, stage: 'test') } - let!(:rubocop_test) { create(:ci_build, pipeline: pipeline, name: 'rubocop', stage_idx: 1, stage: 'test') } - let!(:staging) { create(:ci_build, pipeline: pipeline, name: 'staging', stage_idx: 2, stage: 'deploy') } - - it 'expects to have no dependents if this is first build' do - expect(build.depends_on_builds).to be_empty - end - - it 'expects to have one dependent if this is test' do - expect(rspec_test.depends_on_builds.map(&:id)).to contain_exactly(build.id) - end - - it 'expects to have all builds from build and test stage if this is last' do - expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, rspec_test.id, rubocop_test.id) - end - - it 'expects to have retried builds instead the original ones' do - retried_rspec = Ci::Build.retry(rspec_test) - expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, retried_rspec.id, rubocop_test.id) - end - end - - def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now) - create(factory, source_project_id: pipeline.gl_project_id, - target_project_id: pipeline.gl_project_id, - source_branch: build.ref, - created_at: created_at) - end - - describe '#merge_request' do - context 'when a MR has a reference to the pipeline' do - before do - @merge_request = create_mr(build, pipeline, factory: :merge_request) - - commits = [double(id: pipeline.sha)] - allow(@merge_request).to receive(:commits).and_return(commits) - allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request]) - end - - it 'returns the single associated MR' do - expect(build.merge_request.id).to eq(@merge_request.id) - end - end - - context 'when there is not a MR referencing the pipeline' do - it 'returns nil' do - expect(build.merge_request).to be_nil - end - end - - context 'when more than one MR have a reference to the pipeline' do - before do - @merge_request = create_mr(build, pipeline, factory: :merge_request) - @merge_request.close! - @merge_request2 = create_mr(build, pipeline, factory: :merge_request) - - commits = [double(id: pipeline.sha)] - allow(@merge_request).to receive(:commits).and_return(commits) - allow(@merge_request2).to receive(:commits).and_return(commits) - allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request, @merge_request2]) - end - - it 'returns the first MR' do - expect(build.merge_request.id).to eq(@merge_request.id) - end - end - - context 'when a Build is created after the MR' do - before do - @merge_request = create_mr(build, pipeline, factory: :merge_request_with_diffs) - pipeline2 = create(:ci_pipeline, project: project) - @build2 = create(:ci_build, pipeline: pipeline2) - - allow(@merge_request).to receive(:commits_sha). - and_return([pipeline.sha, pipeline2.sha]) - allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request]) - end - - it 'returns the current MR' do - expect(@build2.merge_request.id).to eq(@merge_request.id) - end - end - end - - describe 'build erasable' do - shared_examples 'erasable' do - it 'removes artifact file' do - expect(build.artifacts_file.exists?).to be_falsy - end - - it 'removes artifact metadata file' do - expect(build.artifacts_metadata.exists?).to be_falsy - end - - it 'erases build trace in trace file' do - expect(build.trace).to be_empty - end - - it 'sets erased to true' do - expect(build.erased?).to be true - end - - it 'sets erase date' do - expect(build.erased_at).not_to be_falsy - end - end - - context 'build is not erasable' do - let!(:build) { create(:ci_build) } - - describe '#erase' do - subject { build.erase } - - it { is_expected.to be false } - end - - describe '#erasable?' do - subject { build.erasable? } - it { is_expected.to eq false } - end - end - - context 'build is erasable' do - let!(:build) { create(:ci_build, :trace, :success, :artifacts) } - - describe '#erase' do - before do - build.erase(erased_by: user) - end - - context 'erased by user' do - let!(:user) { create(:user, username: 'eraser') } - - include_examples 'erasable' - - it 'records user who erased a build' do - expect(build.erased_by).to eq user - end - end - - context 'erased by system' do - let(:user) { nil } - - include_examples 'erasable' - - it 'does not set user who erased a build' do - expect(build.erased_by).to be_nil - end - end - end - - describe '#erasable?' do - subject { build.erasable? } - it { is_expected.to be_truthy } - end - - describe '#erased?' do - let!(:build) { create(:ci_build, :trace, :success, :artifacts) } - subject { build.erased? } - - context 'build has not been erased' do - it { is_expected.to be_falsey } - end - - context 'build has been erased' do - before do - build.erase - end - - it { is_expected.to be_truthy } - end - end - - context 'metadata and build trace are not available' do - let!(:build) { create(:ci_build, :success, :artifacts) } - - before do - build.remove_artifacts_metadata! - end - - describe '#erase' do - it 'does not raise error' do - expect { build.erase }.not_to raise_error - end - end - end - end - end - - describe '#commit' do - it 'returns commit pipeline has been created for' do - expect(build.commit).to eq project.commit - end - end - - describe '#when' do - subject { build.when } - - context 'when `when` is undefined' do - before do - build.when = nil - end - - context 'use from gitlab-ci.yml' do - before do - stub_ci_pipeline_yaml_file(config) - end - - context 'when config is not found' do - let(:config) { nil } - - it { is_expected.to eq('on_success') } - end - - context 'when config does not have a questioned job' do - let(:config) do - YAML.dump({ - test_other: { - script: 'Hello World' - } - }) - end - - it { is_expected.to eq('on_success') } - end - - context 'when config has `when`' do - let(:config) do - YAML.dump({ - test: { - script: 'Hello World', - when: 'always' - } - }) - end - - it { is_expected.to eq('always') } - end - end - end - end - - describe '#cancelable?' do - subject { build } - - context 'when build is cancelable' do - context 'when build is pending' do - it { is_expected.to be_cancelable } - end - - context 'when build is running' do - before do - build.run! - end - - it { is_expected.to be_cancelable } - end - end - - context 'when build is not cancelable' do - context 'when build is successful' do - before do - build.success! - end - - it { is_expected.not_to be_cancelable } - end - - context 'when build is failed' do - before do - build.drop! - end - - it { is_expected.not_to be_cancelable } - end - end - end - - describe '#retryable?' do - subject { build } - - context 'when build is retryable' do - context 'when build is successful' do - before do - build.success! - end - - it { is_expected.to be_retryable } - end - - context 'when build is failed' do - before do - build.drop! - end - - it { is_expected.to be_retryable } - end - - context 'when build is canceled' do - before do - build.cancel! - end - - it { is_expected.to be_retryable } - end - end - - context 'when build is not retryable' do - context 'when build is running' do - before do - build.run! - end - - it { is_expected.not_to be_retryable } - end - - context 'when build is skipped' do - before do - build.skip! - end - - it { is_expected.not_to be_retryable } - end - end - end - - describe '#manual?' do - before do - build.update(when: value) - end - - subject { build.manual? } - - context 'when is set to manual' do - let(:value) { 'manual' } - - it { is_expected.to be_truthy } - end - - context 'when set to something else' do - let(:value) { 'something else' } - - it { is_expected.to be_falsey } - end - end - - describe '#other_actions' do - let(:build) { create(:ci_build, :manual, pipeline: pipeline) } - let!(:other_build) { create(:ci_build, :manual, pipeline: pipeline, name: 'other action') } - - subject { build.other_actions } - - it 'returns other actions' do - is_expected.to contain_exactly(other_build) - end - - context 'when build is retried' do - let!(:new_build) { Ci::Build.retry(build) } - - it 'does not return any of them' do - is_expected.not_to include(build, new_build) - end - end - - context 'when other build is retried' do - let!(:retried_build) { Ci::Build.retry(other_build) } - - it 'returns a retried build' do - is_expected.to contain_exactly(retried_build) - end - end - end - - describe '#play' do - let(:build) { create(:ci_build, :manual, pipeline: pipeline) } - - subject { build.play } - - it 'enqueues a build' do - is_expected.to be_pending - is_expected.to eq(build) - end - - context 'for successful build' do - before do - build.update(status: 'success') - end - - it 'creates a new build' do - is_expected.to be_pending - is_expected.not_to eq(build) - end - end - end - - describe '#when' do - subject { build.when } - - context 'when `when` is undefined' do - before do - build.when = nil - end - - context 'use from gitlab-ci.yml' do - before do - stub_ci_pipeline_yaml_file(config) - end - - context 'when config is not found' do - let(:config) { nil } - - it { is_expected.to eq('on_success') } - end - - context 'when config does not have a questioned job' do - let(:config) do - YAML.dump({ - test_other: { - script: 'Hello World' - } - }) - end - - it { is_expected.to eq('on_success') } - end - - context 'when config has when' do - let(:config) do - YAML.dump({ - test: { - script: 'Hello World', - when: 'always' - } - }) - end - - it { is_expected.to eq('always') } - end - end - end - end - - describe '#retryable?' do - context 'when build is running' do - before { build.run! } - - it 'returns false' do - expect(build).not_to be_retryable - end - end - - context 'when build is finished' do - before do - build.success! - end - - it 'returns true' do - expect(build).to be_retryable - end - end - end - - describe '#has_environment?' do - subject { build.has_environment? } - - context 'when environment is defined' do - before do - build.update(environment: 'review') - end - - it { is_expected.to be_truthy } - end - - context 'when environment is not defined' do - before do - build.update(environment: nil) - end - - it { is_expected.to be_falsey } - end - end - - describe '#starts_environment?' do - subject { build.starts_environment? } - - context 'when environment is defined' do - before do - build.update(environment: 'review') - end - - context 'no action is defined' do - it { is_expected.to be_truthy } - end - - context 'and start action is defined' do - before do - build.update(options: { environment: { action: 'start' } } ) - end - - it { is_expected.to be_truthy } - end - end - - context 'when environment is not defined' do - before do - build.update(environment: nil) - end - - it { is_expected.to be_falsey } - end - end - - describe '#stops_environment?' do - subject { build.stops_environment? } - - context 'when environment is defined' do - before do - build.update(environment: 'review') - end - - context 'no action is defined' do - it { is_expected.to be_falsey } - end - - context 'and stop action is defined' do - before do - build.update(options: { environment: { action: 'stop' } } ) - end - - it { is_expected.to be_truthy } - end - end - - context 'when environment is not defined' do - before do - build.update(environment: nil) - end - - it { is_expected.to be_falsey } - end - end - - describe '#last_deployment' do - subject { build.last_deployment } - - context 'when multiple deployments are created' do - let!(:deployment1) { create(:deployment, deployable: build) } - let!(:deployment2) { create(:deployment, deployable: build) } - - it 'returns the latest one' do - is_expected.to eq(deployment2) - end - end - end - - describe '#outdated_deployment?' do - subject { build.outdated_deployment? } - - context 'when build succeeded' do - let(:build) { create(:ci_build, :success) } - let!(:deployment) { create(:deployment, deployable: build) } - - context 'current deployment is latest' do - it { is_expected.to be_falsey } - end - - context 'current deployment is not latest on environment' do - let!(:deployment2) { create(:deployment, environment: deployment.environment) } - - it { is_expected.to be_truthy } - end - end - - context 'when build failed' do - let(:build) { create(:ci_build, :failed) } - - it { is_expected.to be_falsey } - end - end - - describe '#expanded_environment_name' do - subject { build.expanded_environment_name } - - context 'when environment uses $CI_BUILD_REF_NAME' do - let(:build) do - create(:ci_build, - ref: 'master', - environment: 'review/$CI_BUILD_REF_NAME') - end - - it { is_expected.to eq('review/master') } - end - - context 'when environment uses yaml_variables containing symbol keys' do - let(:build) do - create(:ci_build, - yaml_variables: [{ key: :APP_HOST, value: 'host' }], - environment: 'review/$APP_HOST') - end - - it { is_expected.to eq('review/host') } - end - end - - describe '#detailed_status' do - let(:user) { create(:user) } - - it 'returns a detailed status' do - expect(build.detailed_status(user)) - .to be_a Gitlab::Ci::Status::Build::Cancelable - end - end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 7e1d1126b97..9e4c13bde71 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1,14 +1,963 @@ require 'spec_helper' -describe Ci::Build, models: true do - let(:build) { create(:ci_build) } +describe Ci::Build, :models do + let(:project) { create(:project) } + let(:build) { create(:ci_build, pipeline: pipeline) } let(:test_trace) { 'This is a test' } + let(:pipeline) do + create(:ci_pipeline, project: project, + sha: project.commit.id, + ref: project.default_branch, + status: 'success') + end + it { is_expected.to belong_to(:runner) } it { is_expected.to belong_to(:trigger_request) } it { is_expected.to belong_to(:erased_by) } + it { is_expected.to have_many(:deployments) } + it { is_expected.to validate_presence_of :ref } + it { is_expected.to respond_to :trace_html } + + describe '#any_runners_online?' do + subject { build.any_runners_online? } + + context 'when no runners' do + it { is_expected.to be_falsey } + end + + context 'when there are runners' do + let(:runner) { create(:ci_runner) } + + before do + build.project.runners << runner + runner.update_attributes(contacted_at: 1.second.ago) + end + + it { is_expected.to be_truthy } + + it 'that is inactive' do + runner.update_attributes(active: false) + is_expected.to be_falsey + end + + it 'that is not online' do + runner.update_attributes(contacted_at: nil) + is_expected.to be_falsey + end + + it 'that cannot handle build' do + expect_any_instance_of(Ci::Runner).to receive(:can_pick?).and_return(false) + is_expected.to be_falsey + end + end + end + + describe '#append_trace' do + subject { build.trace_html } + + context 'when build.trace hides runners token' do + let(:token) { 'my_secret_token' } + + before do + build.project.update(runners_token: token) + build.append_trace(token, 0) + end + + it { is_expected.not_to include(token) } + end + + context 'when build.trace hides build token' do + let(:token) { 'my_secret_token' } + + before do + build.update(token: token) + build.append_trace(token, 0) + end + + it { is_expected.not_to include(token) } + end + end + + describe '#artifacts?' do + subject { build.artifacts? } + + context 'artifacts archive does not exist' do + before do + build.update_attributes(artifacts_file: nil) + end + + it { is_expected.to be_falsy } + end + + context 'artifacts archive exists' do + let(:build) { create(:ci_build, :artifacts) } + it { is_expected.to be_truthy } + + context 'is expired' do + before { build.update(artifacts_expire_at: Time.now - 7.days) } + it { is_expected.to be_falsy } + end + + context 'is not expired' do + before { build.update(artifacts_expire_at: Time.now + 7.days) } + it { is_expected.to be_truthy } + end + end + end + + describe '#artifacts_expired?' do + subject { build.artifacts_expired? } + + context 'is expired' do + before { build.update(artifacts_expire_at: Time.now - 7.days) } + + it { is_expected.to be_truthy } + end + + context 'is not expired' do + before { build.update(artifacts_expire_at: Time.now + 7.days) } + + it { is_expected.to be_falsey } + end + end + + describe '#artifacts_metadata?' do + subject { build.artifacts_metadata? } + context 'artifacts metadata does not exist' do + it { is_expected.to be_falsy } + end + + context 'artifacts archive is a zip file and metadata exists' do + let(:build) { create(:ci_build, :artifacts) } + it { is_expected.to be_truthy } + end + end + + describe '#artifacts_expire_in' do + subject { build.artifacts_expire_in } + it { is_expected.to be_nil } + + context 'when artifacts_expire_at is specified' do + let(:expire_at) { Time.now + 7.days } + + before { build.artifacts_expire_at = expire_at } + + it { is_expected.to be_within(5).of(expire_at - Time.now) } + end + end + + describe '#artifacts_expire_in=' do + subject { build.artifacts_expire_in } + + it 'when assigning valid duration' do + build.artifacts_expire_in = '7 days' + + is_expected.to be_within(10).of(7.days.to_i) + end + + it 'when assigning invalid duration' do + expect { build.artifacts_expire_in = '7 elephants' }.to raise_error(ChronicDuration::DurationParseError) + is_expected.to be_nil + end + + it 'when resseting value' do + build.artifacts_expire_in = nil + + is_expected.to be_nil + end + end + + describe '#commit' do + it 'returns commit pipeline has been created for' do + expect(build.commit).to eq project.commit + end + end + + describe '#create_from' do + before do + build.status = 'success' + build.save + end + let(:create_from_build) { Ci::Build.create_from build } + + it 'exists a pending task' do + expect(Ci::Build.pending.count(:all)).to eq 0 + create_from_build + expect(Ci::Build.pending.count(:all)).to be > 0 + end + end + + describe '#depends_on_builds' do + let!(:build) { create(:ci_build, pipeline: pipeline, name: 'build', stage_idx: 0, stage: 'build') } + let!(:rspec_test) { create(:ci_build, pipeline: pipeline, name: 'rspec', stage_idx: 1, stage: 'test') } + let!(:rubocop_test) { create(:ci_build, pipeline: pipeline, name: 'rubocop', stage_idx: 1, stage: 'test') } + let!(:staging) { create(:ci_build, pipeline: pipeline, name: 'staging', stage_idx: 2, stage: 'deploy') } + + it 'expects to have no dependents if this is first build' do + expect(build.depends_on_builds).to be_empty + end + + it 'expects to have one dependent if this is test' do + expect(rspec_test.depends_on_builds.map(&:id)).to contain_exactly(build.id) + end + + it 'expects to have all builds from build and test stage if this is last' do + expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, rspec_test.id, rubocop_test.id) + end + + it 'expects to have retried builds instead the original ones' do + retried_rspec = Ci::Build.retry(rspec_test) + expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, retried_rspec.id, rubocop_test.id) + end + end + + describe '#detailed_status' do + let(:user) { create(:user) } + + it 'returns a detailed status' do + expect(build.detailed_status(user)) + .to be_a Gitlab::Ci::Status::Build::Cancelable + end + end + + describe 'deployment' do + describe '#last_deployment' do + subject { build.last_deployment } + + context 'when multiple deployments are created' do + let!(:deployment1) { create(:deployment, deployable: build) } + let!(:deployment2) { create(:deployment, deployable: build) } + + it 'returns the latest one' do + is_expected.to eq(deployment2) + end + end + end + + describe '#outdated_deployment?' do + subject { build.outdated_deployment? } + + context 'when build succeeded' do + let(:build) { create(:ci_build, :success) } + let!(:deployment) { create(:deployment, deployable: build) } + + context 'current deployment is latest' do + it { is_expected.to be_falsey } + end + + context 'current deployment is not latest on environment' do + let!(:deployment2) { create(:deployment, environment: deployment.environment) } + + it { is_expected.to be_truthy } + end + end + + context 'when build failed' do + let(:build) { create(:ci_build, :failed) } + + it { is_expected.to be_falsey } + end + end + end + + describe 'environment' do + describe '#has_environment?' do + subject { build.has_environment? } + + context 'when environment is defined' do + before do + build.update(environment: 'review') + end + + it { is_expected.to be_truthy } + end + + context 'when environment is not defined' do + before do + build.update(environment: nil) + end + + it { is_expected.to be_falsey } + end + end + + describe '#expanded_environment_name' do + subject { build.expanded_environment_name } + + context 'when environment uses $CI_BUILD_REF_NAME' do + let(:build) do + create(:ci_build, + ref: 'master', + environment: 'review/$CI_BUILD_REF_NAME') + end + + it { is_expected.to eq('review/master') } + end + + context 'when environment uses yaml_variables containing symbol keys' do + let(:build) do + create(:ci_build, + yaml_variables: [{ key: :APP_HOST, value: 'host' }], + environment: 'review/$APP_HOST') + end + + it { is_expected.to eq('review/host') } + end + end + + describe '#starts_environment?' do + subject { build.starts_environment? } + + context 'when environment is defined' do + before do + build.update(environment: 'review') + end + + context 'no action is defined' do + it { is_expected.to be_truthy } + end + + context 'and start action is defined' do + before do + build.update(options: { environment: { action: 'start' } } ) + end + + it { is_expected.to be_truthy } + end + end + + context 'when environment is not defined' do + before do + build.update(environment: nil) + end + + it { is_expected.to be_falsey } + end + end + + describe '#stops_environment?' do + subject { build.stops_environment? } + + context 'when environment is defined' do + before do + build.update(environment: 'review') + end + + context 'no action is defined' do + it { is_expected.to be_falsey } + end + + context 'and stop action is defined' do + before do + build.update(options: { environment: { action: 'stop' } } ) + end + + it { is_expected.to be_truthy } + end + end + + context 'when environment is not defined' do + before do + build.update(environment: nil) + end + + it { is_expected.to be_falsey } + end + end + end + + describe 'erasable build' do + shared_examples 'erasable' do + it 'removes artifact file' do + expect(build.artifacts_file.exists?).to be_falsy + end + + it 'removes artifact metadata file' do + expect(build.artifacts_metadata.exists?).to be_falsy + end + + it 'erases build trace in trace file' do + expect(build.trace).to be_empty + end + + it 'sets erased to true' do + expect(build.erased?).to be true + end + + it 'sets erase date' do + expect(build.erased_at).not_to be_falsy + end + end + + context 'build is not erasable' do + let!(:build) { create(:ci_build) } + + describe '#erase' do + subject { build.erase } + + it { is_expected.to be false } + end + + describe '#erasable?' do + subject { build.erasable? } + it { is_expected.to eq false } + end + end + + context 'build is erasable' do + let!(:build) { create(:ci_build, :trace, :success, :artifacts) } + + describe '#erase' do + before do + build.erase(erased_by: user) + end + + context 'erased by user' do + let!(:user) { create(:user, username: 'eraser') } + + include_examples 'erasable' + + it 'records user who erased a build' do + expect(build.erased_by).to eq user + end + end + + context 'erased by system' do + let(:user) { nil } + + include_examples 'erasable' + + it 'does not set user who erased a build' do + expect(build.erased_by).to be_nil + end + end + end + + describe '#erasable?' do + subject { build.erasable? } + it { is_expected.to be_truthy } + end + + describe '#erased?' do + let!(:build) { create(:ci_build, :trace, :success, :artifacts) } + subject { build.erased? } + + context 'build has not been erased' do + it { is_expected.to be_falsey } + end + + context 'build has been erased' do + before do + build.erase + end + + it { is_expected.to be_truthy } + end + end + + context 'metadata and build trace are not available' do + let!(:build) { create(:ci_build, :success, :artifacts) } + + before do + build.remove_artifacts_metadata! + end + + describe '#erase' do + it 'does not raise error' do + expect { build.erase }.not_to raise_error + end + end + end + end + end + + describe '#extract_coverage' do + context 'valid content & regex' do + subject { build.extract_coverage('Coverage 1033 / 1051 LOC (98.29%) covered', '\(\d+.\d+\%\) covered') } + + it { is_expected.to eq(98.29) } + end + + context 'valid content & bad regex' do + subject { build.extract_coverage('Coverage 1033 / 1051 LOC (98.29%) covered', 'very covered') } + + it { is_expected.to be_nil } + end + + context 'no coverage content & regex' do + subject { build.extract_coverage('No coverage for today :sad:', '\(\d+.\d+\%\) covered') } + + it { is_expected.to be_nil } + end + + context 'multiple results in content & regex' do + subject { build.extract_coverage(' (98.39%) covered. (98.29%) covered', '\(\d+.\d+\%\) covered') } + + it { is_expected.to eq(98.29) } + end + + context 'using a regex capture' do + subject { build.extract_coverage('TOTAL 9926 3489 65%', 'TOTAL\s+\d+\s+\d+\s+(\d{1,3}\%)') } + + it { is_expected.to eq(65) } + end + end + + describe '#first_pending' do + let!(:first) { create(:ci_build, pipeline: pipeline, status: 'pending', created_at: Date.yesterday) } + let!(:second) { create(:ci_build, pipeline: pipeline, status: 'pending') } + subject { Ci::Build.first_pending } + + it { is_expected.to be_a(Ci::Build) } + it('returns with the first pending build') { is_expected.to eq(first) } + end + + describe '#failed_but_allowed?' do + subject { build.failed_but_allowed? } + + context 'when build is not allowed to fail' do + before do + build.allow_failure = false + end + + context 'and build.status is success' do + before do + build.status = 'success' + end + + it { is_expected.to be_falsey } + end + + context 'and build.status is failed' do + before do + build.status = 'failed' + end + + it { is_expected.to be_falsey } + end + end + + context 'when build is allowed to fail' do + before do + build.allow_failure = true + end + + context 'and build.status is success' do + before do + build.status = 'success' + end + + it { is_expected.to be_falsey } + end + + context 'and build.status is failed' do + before do + build.status = 'failed' + end + + it { is_expected.to be_truthy } + end + end + end + + describe 'flags' do + describe '#cancelable?' do + subject { build } + + context 'when build is cancelable' do + context 'when build is pending' do + it { is_expected.to be_cancelable } + end + + context 'when build is running' do + before do + build.run! + end + + it { is_expected.to be_cancelable } + end + end + + context 'when build is not cancelable' do + context 'when build is successful' do + before do + build.success! + end + + it { is_expected.not_to be_cancelable } + end + + context 'when build is failed' do + before do + build.drop! + end + + it { is_expected.not_to be_cancelable } + end + end + end + + describe '#retryable?' do + subject { build } + + context 'when build is retryable' do + context 'when build is successful' do + before do + build.success! + end + + it { is_expected.to be_retryable } + end + + context 'when build is failed' do + before do + build.drop! + end + + it { is_expected.to be_retryable } + end + + context 'when build is canceled' do + before do + build.cancel! + end + + it { is_expected.to be_retryable } + end + end + + context 'when build is not retryable' do + context 'when build is running' do + before do + build.run! + end + + it { is_expected.not_to be_retryable } + end + + context 'when build is skipped' do + before do + build.skip! + end + + it { is_expected.not_to be_retryable } + end + end + end + + describe '#manual?' do + before do + build.update(when: value) + end + + subject { build.manual? } + + context 'when is set to manual' do + let(:value) { 'manual' } + + it { is_expected.to be_truthy } + end + + context 'when set to something else' do + let(:value) { 'something else' } + + it { is_expected.to be_falsey } + end + end + end + + describe '#has_tags?' do + context 'when build has tags' do + subject { create(:ci_build, tag_list: ['tag']) } + it { is_expected.to have_tags } + end + + context 'when build does not have tags' do + subject { create(:ci_build, tag_list: []) } + it { is_expected.not_to have_tags } + end + end + + describe '#keep_artifacts!' do + let(:build) { create(:ci_build, artifacts_expire_at: Time.now + 7.days) } + + it 'to reset expire_at' do + build.keep_artifacts! + + expect(build.artifacts_expire_at).to be_nil + end + end + + describe '#merge_request' do + def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now) + create(factory, source_project_id: pipeline.gl_project_id, + target_project_id: pipeline.gl_project_id, + source_branch: build.ref, + created_at: created_at) + end + + context 'when a MR has a reference to the pipeline' do + before do + @merge_request = create_mr(build, pipeline, factory: :merge_request) + + commits = [double(id: pipeline.sha)] + allow(@merge_request).to receive(:commits).and_return(commits) + allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request]) + end + + it 'returns the single associated MR' do + expect(build.merge_request.id).to eq(@merge_request.id) + end + end + + context 'when there is not a MR referencing the pipeline' do + it 'returns nil' do + expect(build.merge_request).to be_nil + end + end - it { is_expected.to have_many(:deployments) } + context 'when more than one MR have a reference to the pipeline' do + before do + @merge_request = create_mr(build, pipeline, factory: :merge_request) + @merge_request.close! + @merge_request2 = create_mr(build, pipeline, factory: :merge_request) + + commits = [double(id: pipeline.sha)] + allow(@merge_request).to receive(:commits).and_return(commits) + allow(@merge_request2).to receive(:commits).and_return(commits) + allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request, @merge_request2]) + end + + it 'returns the first MR' do + expect(build.merge_request.id).to eq(@merge_request.id) + end + end + + context 'when a Build is created after the MR' do + before do + @merge_request = create_mr(build, pipeline, factory: :merge_request_with_diffs) + pipeline2 = create(:ci_pipeline, project: project) + @build2 = create(:ci_build, pipeline: pipeline2) + + allow(@merge_request).to receive(:commits_sha). + and_return([pipeline.sha, pipeline2.sha]) + allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request]) + end + + it 'returns the current MR' do + expect(@build2.merge_request.id).to eq(@merge_request.id) + end + end + end + + describe '#options' do + let(:options) do + { + image: "ruby:2.1", + services: [ + "postgres" + ] + } + end + + it 'contains options' do + expect(build.options).to eq(options) + end + end + + describe '#other_actions' do + let(:build) { create(:ci_build, :manual, pipeline: pipeline) } + let!(:other_build) { create(:ci_build, :manual, pipeline: pipeline, name: 'other action') } + + subject { build.other_actions } + + it 'returns other actions' do + is_expected.to contain_exactly(other_build) + end + + context 'when build is retried' do + let!(:new_build) { Ci::Build.retry(build) } + + it 'does not return any of them' do + is_expected.not_to include(build, new_build) + end + end + + context 'when other build is retried' do + let!(:retried_build) { Ci::Build.retry(other_build) } + + it 'returns a retried build' do + is_expected.to contain_exactly(retried_build) + end + end + end + + describe '#persisted_environment' do + before do + @environment = create(:environment, project: project, name: "foo-#{project.default_branch}") + end + + subject { build.persisted_environment } + + context 'referenced literally' do + let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-#{project.default_branch}") } + + it { is_expected.to eq(@environment) } + end + + context 'referenced with a variable' do + let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-$CI_BUILD_REF_NAME") } + + it { is_expected.to eq(@environment) } + end + end + + describe '#play' do + let(:build) { create(:ci_build, :manual, pipeline: pipeline) } + + subject { build.play } + + it 'enqueues a build' do + is_expected.to be_pending + is_expected.to eq(build) + end + + context 'for successful build' do + before do + build.update(status: 'success') + end + + it 'creates a new build' do + is_expected.to be_pending + is_expected.not_to eq(build) + end + end + end + + describe 'project settings' do + describe '#timeout' do + it 'returns project timeout configuration' do + expect(build.timeout).to eq(project.build_timeout) + end + end + + + describe '#allow_git_fetch' do + it 'return project allow_git_fetch configuration' do + expect(build.allow_git_fetch).to eq(project.build_allow_git_fetch) + end + end + end + + describe '#project' do + subject { build.project } + + it { is_expected.to eq(pipeline.project) } + end + + describe '#project_id' do + subject { build.project_id } + + it { is_expected.to eq(pipeline.project_id) } + end + + describe '#project_name' do + subject { build.project_name } + + it { is_expected.to eq(project.name) } + end + + describe '#raw_trace' do + subject { build.raw_trace } + + context 'when build.trace hides runners token' do + let(:token) { 'my_secret_token' } + + before do + build.project.update(runners_token: token) + build.update(trace: token) + end + + it { is_expected.not_to include(token) } + end + + context 'when build.trace hides build token' do + let(:token) { 'my_secret_token' } + + before do + build.update(token: token) + build.update(trace: token) + end + + it { is_expected.not_to include(token) } + end + end + + describe '#ref_slug' do + { + 'master' => 'master', + '1-foo' => '1-foo', + 'fix/1-foo' => 'fix-1-foo', + 'fix-1-foo' => 'fix-1-foo', + 'a' * 63 => 'a' * 63, + 'a' * 64 => 'a' * 63, + 'FOO' => 'foo', + }.each do |ref, slug| + it "transforms #{ref} to #{slug}" do + build.ref = ref + + expect(build.ref_slug).to eq(slug) + end + end + end + + describe '#repo_url' do + let(:build) { create(:ci_build) } + let(:project) { build.project } + + subject { build.repo_url } + + it { is_expected.to be_a(String) } + it { is_expected.to end_with(".git") } + it { is_expected.to start_with(project.web_url[0..6]) } + it { is_expected.to include(build.token) } + it { is_expected.to include('gitlab-ci-token') } + it { is_expected.to include(project.web_url[7..-1]) } + end + + describe '#stuck?' do + subject { build.stuck? } + + context "when commit_status.status is pending" do + before do + build.status = 'pending' + end + + it { is_expected.to be_truthy } + + context "and there are specific runner" do + let(:runner) { create(:ci_runner, contacted_at: 1.second.ago) } + + before do + build.project.runners << runner + runner.save + end + + it { is_expected.to be_falsey } + end + end + + %w[success failed canceled running].each do |state| + context "when commit_status.status is #{state}" do + before do + build.status = state + end + + it { is_expected.to be_falsey } + end + end + end describe '#trace' do it 'obfuscates project runners token' do @@ -24,6 +973,45 @@ describe Ci::Build, models: true do expect(build.trace).to eq(test_trace) end + + context 'when build does not have trace' do + it 'is is empty' do + expect(build.trace).to be_nil + end + end + + context 'when trace contains text' do + let(:text) { 'example output' } + before do + build.trace = text + end + + it { expect(build.trace).to eq(text) } + end + + context 'when trace hides runners token' do + let(:token) { 'my_secret_token' } + + before do + build.update(trace: token) + build.project.update(runners_token: token) + end + + it { expect(build.trace).not_to include(token) } + it { expect(build.raw_trace).to include(token) } + end + + context 'when build.trace hides build token' do + let(:token) { 'my_secret_token' } + + before do + build.update(trace: token) + build.update(token: token) + end + + it { expect(build.trace).not_to include(token) } + it { expect(build.raw_trace).to include(token) } + end end describe '#has_trace_file?' do @@ -111,4 +1099,289 @@ describe Ci::Build, models: true do build.destroy end end + + describe '#when' do + subject { build.when } + + context 'when `when` is undefined' do + before do + build.when = nil + end + + context 'use from gitlab-ci.yml' do + before do + stub_ci_pipeline_yaml_file(config) + end + + context 'when config is not found' do + let(:config) { nil } + + it { is_expected.to eq('on_success') } + end + + context 'when config does not have a questioned job' do + let(:config) do + YAML.dump({ + test_other: { + script: 'Hello World' + } + }) + end + + it { is_expected.to eq('on_success') } + end + + context 'when config has `when`' do + let(:config) do + YAML.dump({ + test: { + script: 'Hello World', + when: 'always' + } + }) + end + + it { is_expected.to eq('always') } + end + end + end + end + + describe '#variables' do + let(:container_registry_enabled) { false } + let(:predefined_variables) do + [ + { key: 'CI', value: 'true', public: true }, + { key: 'GITLAB_CI', value: 'true', public: true }, + { key: 'CI_BUILD_ID', value: build.id.to_s, public: true }, + { key: 'CI_BUILD_TOKEN', value: build.token, public: false }, + { key: 'CI_BUILD_REF', value: build.sha, public: true }, + { key: 'CI_BUILD_BEFORE_SHA', value: build.before_sha, public: true }, + { key: 'CI_BUILD_REF_NAME', value: 'master', public: true }, + { key: 'CI_BUILD_REF_SLUG', value: 'master', public: true }, + { key: 'CI_BUILD_NAME', value: 'test', public: true }, + { key: 'CI_BUILD_STAGE', value: 'test', public: true }, + { key: 'CI_SERVER_NAME', value: 'GitLab', public: true }, + { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true }, + { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true }, + { key: 'CI_PROJECT_ID', value: project.id.to_s, public: true }, + { key: 'CI_PROJECT_NAME', value: project.path, public: true }, + { key: 'CI_PROJECT_PATH', value: project.path_with_namespace, public: true }, + { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.path, public: true }, + { key: 'CI_PROJECT_URL', value: project.web_url, public: true }, + { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true } + ] + end + + before do + stub_container_registry_config(enabled: container_registry_enabled, host_port: 'registry.example.com') + end + + subject { build.variables } + + context 'returns variables' do + before do + build.yaml_variables = [] + end + + it { is_expected.to eq(predefined_variables) } + end + + context 'when build has user' do + let(:user) { create(:user, username: 'starter') } + let(:user_variables) do + [ + { key: 'GITLAB_USER_ID', value: user.id.to_s, public: true }, + { key: 'GITLAB_USER_EMAIL', value: user.email, public: true } + ] + end + + before do + build.update_attributes(user: user) + end + + it { user_variables.each { |v| is_expected.to include(v) } } + end + + context 'when build has an environment' do + before do + build.update(environment: 'production') + create(:environment, project: build.project, name: 'production', slug: 'prod-slug') + end + + let(:environment_variables) do + [ + { key: 'CI_ENVIRONMENT_NAME', value: 'production', public: true }, + { key: 'CI_ENVIRONMENT_SLUG', value: 'prod-slug', public: true } + ] + end + + it { environment_variables.each { |v| is_expected.to include(v) } } + end + + context 'when build started manually' do + before do + build.update_attributes(when: :manual) + end + + let(:manual_variable) do + { key: 'CI_BUILD_MANUAL', value: 'true', public: true } + end + + it { is_expected.to include(manual_variable) } + end + + context 'when build is for tag' do + let(:tag_variable) do + { key: 'CI_BUILD_TAG', value: 'master', public: true } + end + + before do + build.update_attributes(tag: true) + end + + it { is_expected.to include(tag_variable) } + end + + context 'when secure variable is defined' do + let(:secure_variable) do + { key: 'SECRET_KEY', value: 'secret_value', public: false } + end + + before do + build.project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value') + end + + it { is_expected.to include(secure_variable) } + end + + context 'when build is for triggers' do + let(:trigger) { create(:ci_trigger, project: project) } + let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger) } + let(:user_trigger_variable) do + { key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1', public: false } + end + let(:predefined_trigger_variable) do + { key: 'CI_BUILD_TRIGGERED', value: 'true', public: true } + end + + before do + build.trigger_request = trigger_request + end + + it { is_expected.to include(user_trigger_variable) } + it { is_expected.to include(predefined_trigger_variable) } + end + + context 'when yaml_variables are undefined' do + before do + build.yaml_variables = nil + end + + context 'use from gitlab-ci.yml' do + before do + stub_ci_pipeline_yaml_file(config) + end + + context 'when config is not found' do + let(:config) { nil } + + it { is_expected.to eq(predefined_variables) } + end + + context 'when config does not have a questioned job' do + let(:config) do + YAML.dump({ + test_other: { + script: 'Hello World' + } + }) + end + + it { is_expected.to eq(predefined_variables) } + end + + context 'when config has variables' do + let(:config) do + YAML.dump({ + test: { + script: 'Hello World', + variables: { + KEY: 'value' + } + } + }) + end + let(:variables) do + [{ key: 'KEY', value: 'value', public: true }] + end + + it { is_expected.to eq(predefined_variables + variables) } + end + end + end + + context 'when container registry is enabled' do + let(:container_registry_enabled) { true } + let(:ci_registry) do + { key: 'CI_REGISTRY', value: 'registry.example.com', public: true } + end + let(:ci_registry_image) do + { key: 'CI_REGISTRY_IMAGE', value: project.container_registry_repository_url, public: true } + end + + context 'and is disabled for project' do + before do + project.update(container_registry_enabled: false) + end + + it { is_expected.to include(ci_registry) } + it { is_expected.not_to include(ci_registry_image) } + end + + context 'and is enabled for project' do + before do + project.update(container_registry_enabled: true) + end + + it { is_expected.to include(ci_registry) } + it { is_expected.to include(ci_registry_image) } + end + end + + context 'when runner is assigned to build' do + let(:runner) { create(:ci_runner, description: 'description', tag_list: ['docker', 'linux']) } + + before do + build.update(runner: runner) + end + + it { is_expected.to include({ key: 'CI_RUNNER_ID', value: runner.id.to_s, public: true }) } + it { is_expected.to include({ key: 'CI_RUNNER_DESCRIPTION', value: 'description', public: true }) } + it { is_expected.to include({ key: 'CI_RUNNER_TAGS', value: 'docker, linux', public: true }) } + end + + context 'when build is for a deployment' do + let(:deployment_variable) { { key: 'KUBERNETES_TOKEN', value: 'TOKEN', public: false } } + + before do + build.environment = 'production' + allow(project).to receive(:deployment_variables).and_return([deployment_variable]) + end + + it { is_expected.to include(deployment_variable) } + end + + context 'returns variables in valid order' do + before do + allow(build).to receive(:predefined_variables) { ['predefined'] } + allow(project).to receive(:predefined_variables) { ['project'] } + allow(pipeline).to receive(:predefined_variables) { ['pipeline'] } + allow(build).to receive(:yaml_variables) { ['yaml'] } + allow(project).to receive(:secret_variables) { ['secret'] } + end + + it { is_expected.to eq(%w[predefined project pipeline yaml secret]) } + end + end end -- cgit v1.2.1 From 908443868721e0ea0940086fe39fd433feae2891 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 13 Jan 2017 15:35:33 +0100 Subject: Remove empty build spec file from invalid location --- spec/models/build_spec.rb | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 spec/models/build_spec.rb diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb deleted file mode 100644 index 64c751e791f..00000000000 --- a/spec/models/build_spec.rb +++ /dev/null @@ -1,6 +0,0 @@ -require 'spec_helper' - -describe Ci::Build, models: true do - - -end -- cgit v1.2.1 From c47e1f97fdc1328461e965471f5aab100e337285 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 13 Jan 2017 16:47:00 +0100 Subject: Add Changelog for commit links fix on pipelines page --- changelogs/unreleased/fix-serialized-commit-path.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/fix-serialized-commit-path.yml diff --git a/changelogs/unreleased/fix-serialized-commit-path.yml b/changelogs/unreleased/fix-serialized-commit-path.yml new file mode 100644 index 00000000000..4e4df503874 --- /dev/null +++ b/changelogs/unreleased/fix-serialized-commit-path.yml @@ -0,0 +1,4 @@ +--- +title: Fix links to commits pages on pipelines list page +merge_request: 8558 +author: -- cgit v1.2.1 From d69ec1269a0fa321d10504e906724f49eaab4a7b Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 13 Jan 2017 16:53:00 +0100 Subject: Fix Rubocop offense in build specs --- spec/models/ci/build_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 9e4c13bde71..af0f6a31eda 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -842,7 +842,6 @@ describe Ci::Build, :models do end end - describe '#allow_git_fetch' do it 'return project allow_git_fetch configuration' do expect(build.allow_git_fetch).to eq(project.build_allow_git_fetch) -- cgit v1.2.1 From 45485947e255c11ca6a45bddf0ba22e9f7dfdea5 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Thu, 22 Dec 2016 20:20:12 +0000 Subject: Improve disabled state select Add tests --- .../projects/mattermosts/_team_selection.html.haml | 11 +-- .../services/mattermost_slash_command_spec.rb | 85 +++++++++++++++++++++- 2 files changed, 88 insertions(+), 8 deletions(-) diff --git a/app/views/projects/mattermosts/_team_selection.html.haml b/app/views/projects/mattermosts/_team_selection.html.haml index 24e86b8497f..a80f9aa4c4a 100644 --- a/app/views/projects/mattermosts/_team_selection.html.haml +++ b/app/views/projects/mattermosts/_team_selection.html.haml @@ -7,20 +7,21 @@ %p = @teams.one? ? 'The team' : 'Select the team' where the slash commands will be used in - - selected_id = @teams.keys.first if @teams.one? + - selected_id = @teams.one? ? @teams.keys.first : 0 - options = mattermost_teams_options(@teams) - options = options_for_select(options, selected_id) - = f.select(:team_id, options, {}, { class: 'form-control', selected: "#{selected_id}" }) + = f.select(:team_id, options, {}, { class: 'form-control', disabled: @teams.one?, selected: selected_id }) + = f.hidden_field(:team_id, value: selected_id) if @teams.one? .help-block - if @teams.one? - This is the only team where you are an administrator. + This is the only available team. - else - The list shows teams where you are administrator - To create a team, ask your Mattermost system administrator. + The list shows all available teams. To create a team, = link_to "#{Gitlab.config.mattermost.host}/create_team" do use Mattermost's interface = icon('external-link') + or ask your Mattermost system administrator. %hr %h4 Command trigger word %p Choose the word that will trigger commands diff --git a/spec/features/projects/services/mattermost_slash_command_spec.rb b/spec/features/projects/services/mattermost_slash_command_spec.rb index 8de827447ff..86a07b2c679 100644 --- a/spec/features/projects/services/mattermost_slash_command_spec.rb +++ b/spec/features/projects/services/mattermost_slash_command_spec.rb @@ -33,10 +33,89 @@ feature 'Setup Mattermost slash commands', feature: true do expect(value).to eq(token) end - describe 'mattermost service is enabled' do - it 'shows the add to mattermost button' do - expect(page).to have_link 'Add to Mattermost' + it 'shows the add to mattermost button' do + expect(page).to have_link('Add to Mattermost') + end + + it 'shows an explanation if user is a member of no teams' do + stub_teams(count: 0) + + click_link 'Add to Mattermost' + + expect(page).to have_content('You aren’t a member of any team on the Mattermost instance') + expect(page).to have_link('join a team', href: "#{Gitlab.config.mattermost.host}/select_team") + end + + it 'shows an explanation if user is a member of 1 team' do + stub_teams(count: 1) + + click_link 'Add to Mattermost' + + expect(page).to have_content('The team where the slash commands will be used in') + expect(page).to have_content('This is the only available team.') + end + + it 'shows a disabled prefilled select if user is a member of 1 team' do + teams = stub_teams(count: 1) + + click_link 'Add to Mattermost' + + team_name = teams.first[1]['display_name'] + select_element = find('select#mattermost_team_id') + selected_option = select_element.find('option[selected]') + + expect(select_element['disabled']).to be(true) + expect(selected_option).to have_content(team_name.to_s) + end + + it 'has a hidden input for the prefilled value if user is a member of 1 team' do + teams = stub_teams(count: 1) + + click_link 'Add to Mattermost' + + expect(find('input#mattermost_team_id', visible: false).value).to eq(teams.first[0].to_s) + end + + it 'shows an explanation user is a member of multiple teams' do + stub_teams(count: 2) + + click_link 'Add to Mattermost' + + expect(page).to have_content('Select the team where the slash commands will be used in') + expect(page).to have_content('The list shows all available teams.') + end + + it 'shows a select with team options user is a member of multiple teams' do + stub_teams(count: 2) + + click_link 'Add to Mattermost' + + select_element = find('select#mattermost_team_id') + selected_option = select_element.find('option[selected]') + + expect(select_element['disabled']).to be(false) + expect(selected_option).to have_content('Select team...') + # The 'Select team...' placeholder is item `0`. + expect(select_element.all('option').count).to eq(3) + end + + def stub_teams(count: 0) + teams = create_teams(count) + + allow_any_instance_of(MattermostSlashCommandsService).to receive(:list_teams) { teams } + + teams + end + + def create_teams(count = 0) + teams = {} + + count.times do |i| + i += 1 + teams[i] = { id: i, display_name: i } end + + teams end describe 'mattermost service is not enabled' do -- cgit v1.2.1 From c5a373c634a0547a204c35fec8fd2c4fe4acbca8 Mon Sep 17 00:00:00 2001 From: Regis Date: Fri, 13 Jan 2017 12:59:32 -0500 Subject: fix pagination component handling different header styles from different server proxies --- .../javascripts/vue_pipelines_index/store.js.es6 | 26 +++++++++++++++------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/vue_pipelines_index/store.js.es6 b/app/assets/javascripts/vue_pipelines_index/store.js.es6 index 6b34839b030..1982142853a 100644 --- a/app/assets/javascripts/vue_pipelines_index/store.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/store.js.es6 @@ -3,14 +3,24 @@ /*= require vue_realtime_listener/index.js */ ((gl) => { - const pageValues = headers => ({ - perPage: +headers['X-Per-Page'], - page: +headers['X-Page'], - total: +headers['X-Total'], - totalPages: +headers['X-Total-Pages'], - nextPage: +headers['X-Next-Page'], - previousPage: +headers['X-Prev-Page'], - }); + const pageValues = (headers) => { + const normalizedHeaders = {}; + + Object.keys(headers).forEach((e) => { + normalizedHeaders[e.toUpperCase()] = headers[e]; + }); + + const paginationInfo = { + perPage: +normalizedHeaders['X-PER-PAGE'], + page: +normalizedHeaders['X-PAGE'], + total: +normalizedHeaders['X-TOTAL'], + totalPages: +normalizedHeaders['X-TOTAL-PAGES'], + nextPage: +normalizedHeaders['X-NEXT-PAGE'], + previousPage: +normalizedHeaders['X-PREV-PAGE'], + }; + + return paginationInfo; + }; gl.PipelineStore = class { fetchDataLoop(Vue, pageNum, url, apiScope) { -- cgit v1.2.1 From 0bc48797c8559a1f9c042b74e2faadc820fc7257 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 13 Jan 2017 14:54:25 -0500 Subject: revise sortable_name test formatting --- spec/models/commit_status_spec.rb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index daabc804d16..64ea607eb95 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -245,8 +245,6 @@ describe CommitStatus, models: true do end describe '#sortable_name' do - subject { commit_status.sortable_name } - tests = { 'karma' => ['karma'], 'karma 0 20' => ['karma ', 0, ' ', 20], @@ -260,8 +258,7 @@ describe CommitStatus, models: true do tests.each do |name, sortable_name| it "'#{name}' sorts as '#{sortable_name}'" do commit_status.name = name - - is_expected.to eq(sortable_name) + expect(commit_status.sortable_name).to eq(sortable_name) end end end -- cgit v1.2.1 From ebc1d5b39d02ddf3d5a94e968084ca67a59603e2 Mon Sep 17 00:00:00 2001 From: Regis Date: Sun, 15 Jan 2017 00:11:12 -0500 Subject: change how pagination component is loaded --- app/assets/javascripts/vue_pipelines_index/index.js.es6 | 1 + app/views/projects/pipelines/index.html.haml | 1 - config/application.rb | 1 - 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/assets/javascripts/vue_pipelines_index/index.js.es6 b/app/assets/javascripts/vue_pipelines_index/index.js.es6 index 9dfbedd73ab..edd01f17a97 100644 --- a/app/assets/javascripts/vue_pipelines_index/index.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/index.js.es6 @@ -1,5 +1,6 @@ /* global Vue, VueResource, gl */ /*= require vue_common_component/commit */ +/*= require vue_pagination/index */ /*= require vue-resource /*= require boards/vue_resource_interceptor */ /*= require ./status.js.es6 */ diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index abea6932567..df36279ed75 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -64,5 +64,4 @@ .vue-pipelines-index -= page_specific_javascript_tag('vue_pagination/index.js') = page_specific_javascript_tag('vue_pipelines_index/index.js') diff --git a/config/application.rb b/config/application.rb index aa52b0cd512..8ce549cebf6 100644 --- a/config/application.rb +++ b/config/application.rb @@ -111,7 +111,6 @@ module Gitlab config.assets.precompile << "lib/*.js" config.assets.precompile << "u2f.js" config.assets.precompile << "vue_pipelines_index/index.js" - config.assets.precompile << "vue_pagination/index.js" config.assets.precompile << "vendor/assets/fonts/*" # Version of your assets, change this if you want to expire all your assets -- cgit v1.2.1 From de460f7e4d06bec7bd6138c2b4edc956e9df4471 Mon Sep 17 00:00:00 2001 From: Semyon Pupkov Date: Sun, 15 Jan 2017 12:08:29 +0500 Subject: Use string for class_name option for lazy autoload class Passing constant into this option is deprecated in Rails 5.2 https://github.com/rails/rails/commit/8312a0d22212798864f142b5a94805e0baa6c562 --- app/models/forked_project_link.rb | 4 ++-- app/models/project.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/forked_project_link.rb b/app/models/forked_project_link.rb index 9803bae0bee..36cf7ad6a28 100644 --- a/app/models/forked_project_link.rb +++ b/app/models/forked_project_link.rb @@ -1,4 +1,4 @@ class ForkedProjectLink < ActiveRecord::Base - belongs_to :forked_to_project, class_name: Project - belongs_to :forked_from_project, class_name: Project + belongs_to :forked_to_project, class_name: 'Project' + belongs_to :forked_from_project, class_name: 'Project' end diff --git a/app/models/project.rb b/app/models/project.rb index c22386c84e9..e85d3d3bc6c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -122,7 +122,7 @@ class Project < ActiveRecord::Base # Merge Requests for target project should be removed with it has_many :merge_requests, dependent: :destroy, foreign_key: 'target_project_id' # Merge requests from source project should be kept when source project was removed - has_many :fork_merge_requests, foreign_key: 'source_project_id', class_name: MergeRequest + has_many :fork_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest' has_many :issues, dependent: :destroy has_many :labels, dependent: :destroy, class_name: 'ProjectLabel' has_many :services, dependent: :destroy -- cgit v1.2.1 From ad977e8bb257db6ddfae022795a94a4ea95f9be3 Mon Sep 17 00:00:00 2001 From: blackst0ne Date: Sun, 15 Jan 2017 19:48:35 +1100 Subject: Allow to use + symbol in filenames --- changelogs/unreleased/allow_plus_sign_for_snippets.yml | 4 ++++ lib/gitlab/regex.rb | 4 ++-- spec/features/snippets/create_snippet_spec.rb | 14 ++++++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/allow_plus_sign_for_snippets.yml diff --git a/changelogs/unreleased/allow_plus_sign_for_snippets.yml b/changelogs/unreleased/allow_plus_sign_for_snippets.yml new file mode 100644 index 00000000000..62d9dd74d07 --- /dev/null +++ b/changelogs/unreleased/allow_plus_sign_for_snippets.yml @@ -0,0 +1,4 @@ +--- +title: Allow to use + symbol in filenames +merge_request: 6644 +author: blackst0ne diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index 9e0b0e5ea98..a3fa7c1331a 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -61,11 +61,11 @@ module Gitlab end def file_name_regex - @file_name_regex ||= /\A[[[:alnum:]]_\-\.\@]*\z/.freeze + @file_name_regex ||= /\A[[[:alnum:]]_\-\.\@\+]*\z/.freeze end def file_name_regex_message - "can contain only letters, digits, '_', '-', '@' and '.'." + "can contain only letters, digits, '_', '-', '@', '+' and '.'." end def file_path_regex diff --git a/spec/features/snippets/create_snippet_spec.rb b/spec/features/snippets/create_snippet_spec.rb index cb95e7828db..5470276bf06 100644 --- a/spec/features/snippets/create_snippet_spec.rb +++ b/spec/features/snippets/create_snippet_spec.rb @@ -17,4 +17,18 @@ feature 'Create Snippet', feature: true do expect(page).to have_content('My Snippet Title') expect(page).to have_content('Hello World!') end + + scenario 'Authenticated user creates a snippet with + in filename' do + fill_in 'personal_snippet_title', with: 'My Snippet Title' + page.within('.file-editor') do + find(:xpath, "//input[@id='personal_snippet_file_name']").set 'snippet+file+name' + find(:xpath, "//input[@id='personal_snippet_content']").set 'Hello World!' + end + + click_button 'Create snippet' + + expect(page).to have_content('My Snippet Title') + expect(page).to have_content('snippet+file+name') + expect(page).to have_content('Hello World!') + end end -- cgit v1.2.1
StatusPipelineCommitStagesStatusPipelineCommitStages