From 7a9ba9bb85c1ab0e4bb4f116ce45b9a82aea3096 Mon Sep 17 00:00:00 2001 From: winniehell Date: Mon, 5 Dec 2016 23:02:23 +0100 Subject: [PATCH 001/191] Add failing test for #25191 --- spec/lib/banzai/filter/relative_link_filter_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb index 2bfa51deb20634..df2dd173b5786d 100644 --- a/spec/lib/banzai/filter/relative_link_filter_spec.rb +++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb @@ -175,7 +175,7 @@ def link(path) allow_any_instance_of(described_class).to receive(:uri_type).and_return(:raw) doc = filter(image(escaped)) - expect(doc.at_css('img')['src']).to match '/raw/' + expect(doc.at_css('img')['src']).to eq "/#{project_path}/raw/#{Addressable::URI.escape(ref)}/#{escaped}" end context 'when requested path is a file in the repo' do -- GitLab From 593c912151eaef865d31fc2a8307ef6d337c2349 Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Mon, 5 Dec 2016 15:40:53 +0100 Subject: [PATCH 002/191] Grapify the service API --- .../project_services/buildkite_service.rb | 3 +- .../project_services/drone_ci_service.rb | 3 +- .../emails_on_push_service.rb | 18 +- .../project_services/hipchat_service.rb | 6 +- app/models/project_services/irker_service.rb | 3 +- doc/api/services.md | 102 ++- lib/api/helpers.rb | 11 - lib/api/services.rb | 626 ++++++++++++++++-- .../project_services/hipchat_service_spec.rb | 2 +- spec/requests/api/services_spec.rb | 5 +- spec/support/services_shared_context.rb | 6 + 11 files changed, 700 insertions(+), 85 deletions(-) diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb index 86a06321e212d4..fe6d7aabb22e45 100644 --- a/app/models/project_services/buildkite_service.rb +++ b/app/models/project_services/buildkite_service.rb @@ -3,7 +3,8 @@ class BuildkiteService < CiService ENDPOINT = "https://buildkite.com" - prop_accessor :project_url, :token, :enable_ssl_verification + prop_accessor :project_url, :token + boolean_accessor :enable_ssl_verification validates :project_url, presence: true, url: true, if: :activated? validates :token, presence: true, if: :activated? diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb index 5e4dd101c53e6a..adc78a427ee30e 100644 --- a/app/models/project_services/drone_ci_service.rb +++ b/app/models/project_services/drone_ci_service.rb @@ -1,5 +1,6 @@ class DroneCiService < CiService - prop_accessor :drone_url, :token, :enable_ssl_verification + prop_accessor :drone_url, :token + boolean_accessor :enable_ssl_verification validates :drone_url, presence: true, url: true, if: :activated? validates :token, presence: true, if: :activated? diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb index e0083c43adb260..79285cbd26de11 100644 --- a/app/models/project_services/emails_on_push_service.rb +++ b/app/models/project_services/emails_on_push_service.rb @@ -1,6 +1,6 @@ class EmailsOnPushService < Service - prop_accessor :send_from_committer_email - prop_accessor :disable_diffs + boolean_accessor :send_from_committer_email + boolean_accessor :disable_diffs prop_accessor :recipients validates :recipients, presence: true, if: :activated? @@ -24,20 +24,20 @@ def execute(push_data) return unless supported_events.include?(push_data[:object_kind]) EmailsOnPushWorker.perform_async( - project_id, - recipients, - push_data, - send_from_committer_email: send_from_committer_email?, - disable_diffs: disable_diffs? + project_id, + recipients, + push_data, + send_from_committer_email: send_from_committer_email?, + disable_diffs: disable_diffs? ) end def send_from_committer_email? - self.send_from_committer_email == "1" + Gitlab::Utils.to_boolean(self.send_from_committer_email) end def disable_diffs? - self.disable_diffs == "1" + Gitlab::Utils.to_boolean(self.disable_diffs) end def fields diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 660a8ae3421ec1..915f6fed74c01c 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -8,8 +8,8 @@ class HipchatService < Service ul ol li dl dt dd ] - prop_accessor :token, :room, :server, :notify, :color, :api_version - boolean_accessor :notify_only_broken_builds + prop_accessor :token, :room, :server, :color, :api_version + boolean_accessor :notify_only_broken_builds, :notify validates :token, presence: true, if: :activated? def initialize_properties @@ -75,7 +75,7 @@ def gate end def message_options(data = nil) - { notify: notify.present? && notify == '1', color: message_color(data) } + { notify: notify.present? && Gitlab::Utils.to_boolean(notify), color: message_color(data) } end def create_message(data) diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb index ce7d1c5d5b136c..7355918feab407 100644 --- a/app/models/project_services/irker_service.rb +++ b/app/models/project_services/irker_service.rb @@ -2,7 +2,8 @@ class IrkerService < Service prop_accessor :server_host, :server_port, :default_irc_uri - prop_accessor :colorize_messages, :recipients, :channels + prop_accessor :recipients, :channels + boolean_accessor :colorize_messages validates :recipients, presence: true, if: :activated? before_validation :get_channels diff --git a/doc/api/services.md b/doc/api/services.md index a5d733fe6c7ebe..acb54448664b00 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -139,6 +139,40 @@ Get Buildkite service settings for a project. GET /projects/:id/services/buildkite ``` +## Build-Emails + +Get emails for GitLab CI builds. + +### Create/Edit Build-Emails service + +Set Build-Emails service for a project. + +``` +PUT /projects/:id/services/builds-email +``` + +Parameters: + +- `recipients` (**required**) - Comma-separated list of recipient email addresses +- `add_pusher` (optional) - Add pusher to recipients list +- `notify_only_broken_builds` (optional) -Notify only broken builds + +### Delete Build-Emails service + +Delete Build-Emails service for a project. + +``` +DELETE /projects/:id/services/builds-email +``` + +### Get Build-Emails service settings + +Get Build-Emails service settings for a project. + +``` +GET /projects/:id/services/builds-email +``` + ## Campfire Simple web-based real-time group chat @@ -476,12 +510,11 @@ PUT /projects/:id/services/jira | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `active` | boolean| no | Enable/disable the JIRA service. | | `url` | string | yes | The URL to the JIRA project which is being linked to this GitLab project, e.g., `https://jira.example.com`. | | `project_key` | string | yes | The short identifier for your JIRA project, all uppercase, e.g., `PROJ`. | | `username` | string | no | The username of the user created to be used with GitLab/JIRA. | | `password` | string | no | The password of the user created to be used with GitLab/JIRA. | -| `jira_issue_transition_id` | string | no | The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. | +| `jira_issue_transition_id` | integer | no | The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. | ### Delete JIRA service @@ -491,6 +524,71 @@ Remove all previously JIRA settings from a project. DELETE /projects/:id/services/jira ``` +## Mattermost Slash Commands + +Ability to receive slash commands from a Mattermost chat instance. + +### Create/Edit Mattermost Slash Command service + +Set Mattermost Slash Command for a project. + +``` +PUT /projects/:id/services/mattermost-slash-commands +``` + +Parameters: + +- `token` (**required**) - The Mattermost token + +### Delete Mattermost Slash Command service + +Delete Mattermost Slash Command service for a project. + +``` +DELETE /projects/:id/services/mattermost-slash-commands +``` + +### Get Mattermost Slash Command service settings + +Get Mattermost Slash Command service settings for a project. + +``` +GET /projects/:id/services/mattermost-slash-commands +``` + +## Pipeline-Emails + +Get emails for GitLab CI pipelines. + +### Create/Edit Pipeline-Emails service + +Set Pipeline-Emails service for a project. + +``` +PUT /projects/:id/services/pipelines-email +``` + +Parameters: + +- `recipients` (**required**) - Comma-separated list of recipient email addresses +- `notify_only_broken_builds` (optional) -Notify only broken pipelines + +### Delete Pipeline-Emails service + +Delete Pipeline-Emails service for a project. + +``` +DELETE /projects/:id/services/pipelines-email +``` + +### Get Pipeline-Emails service settings + +Get Pipeline-Emails service settings for a project. + +``` +GET /projects/:id/services/pipelines-email +``` + ## PivotalTracker Project Management Software (Source Commits Endpoint) diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 7f94ede7940a81..16ac40b7142824 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -93,17 +93,6 @@ def find_project!(id) end end - def project_service(project = user_project) - @project_service ||= project.find_or_initialize_service(params[:service_slug].underscore) - @project_service || not_found!("Service") - end - - def service_attributes - @service_attributes ||= project_service.fields.inject([]) do |arr, hash| - arr << hash[:name].to_sym - end - end - def find_group(id) if id =~ /^\d+$/ Group.find_by(id: id) diff --git a/lib/api/services.rb b/lib/api/services.rb index bc427705777fc3..fde2e2746f1bb1 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -1,84 +1,602 @@ module API - # Projects API class Services < Grape::API + services = { + 'asana' => [ + { + required: true, + name: :api_key, + type: String, + desc: 'User API token' + }, + { + required: false, + name: :restrict_to_branch, + type: String, + desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches' + } + ], + 'assembla' => [ + { + required: true, + name: :token, + type: String, + desc: 'The authentication token' + }, + { + required: false, + name: :subdomain, + type: String, + desc: 'Subdomain setting' + } + ], + 'bamboo' => [ + { + required: true, + name: :bamboo_url, + type: String, + desc: 'Bamboo root URL like https://bamboo.example.com' + }, + { + required: true, + name: :build_key, + type: String, + desc: 'Bamboo build plan key like' + }, + { + required: true, + name: :username, + type: String, + desc: 'A user with API access, if applicable' + }, + { + required: true, + name: :password, + type: String, + desc: 'Passord of the user' + } + ], + 'bugzilla' => [ + { + required: true, + name: :new_issue_url, + type: String, + desc: 'New issue URL' + }, + { + required: true, + name: :issues_url, + type: String, + desc: 'Issues URL' + }, + { + required: true, + name: :project_url, + type: String, + desc: 'Project URL' + }, + { + required: false, + name: :description, + type: String, + desc: 'Description' + }, + { + required: false, + name: :title, + type: String, + desc: 'Title' + } + ], + 'buildkite' => [ + { + required: true, + name: :token, + type: String, + desc: 'Buildkite project GitLab token' + }, + { + required: true, + name: :project_url, + type: String, + desc: 'The buildkite project URL' + }, + { + required: false, + name: :enable_ssl_verification, + type: Boolean, + desc: 'Enable SSL verification for communication' + } + ], + 'builds-email' => [ + { + required: true, + name: :recipients, + type: String, + desc: 'Comma-separated list of recipient email addresses' + }, + { + required: false, + name: :add_pusher, + type: Boolean, + desc: 'Add pusher to recipients list' + }, + { + required: false, + name: :notify_only_broken_builds, + type: Boolean, + desc: 'Notify only broken builds' + } + ], + 'campfire' => [ + { + required: true, + name: :token, + type: String, + desc: 'Campfire token' + }, + { + required: false, + name: :subdomain, + type: String, + desc: 'Campfire subdomain' + }, + { + required: false, + name: :room, + type: String, + desc: 'Campfire room' + }, + ], + 'custom-issue-tracker' => [ + { + required: true, + name: :new_issue_url, + type: String, + desc: 'New issue URL' + }, + { + required: true, + name: :issues_url, + type: String, + desc: 'Issues URL' + }, + { + required: true, + name: :project_url, + type: String, + desc: 'Project URL' + }, + { + required: false, + name: :description, + type: String, + desc: 'Description' + }, + { + required: false, + name: :title, + type: String, + desc: 'Title' + } + ], + 'drone-ci' => [ + { + required: true, + name: :token, + type: String, + desc: 'Drone CI token' + }, + { + required: true, + name: :drone_url, + type: String, + desc: 'Drone CI URL' + }, + { + required: false, + name: :enable_ssl_verification, + type: Boolean, + desc: 'Enable SSL verification for communication' + } + ], + 'emails-on-push' => [ + { + required: true, + name: :recipients, + type: String, + desc: 'Comma-separated list of recipient email addresses' + }, + { + required: false, + name: :disable_diffs, + type: Boolean, + desc: 'Disable code diffs' + }, + { + required: false, + name: :send_from_committer_email, + type: Boolean, + desc: 'Send from committer' + } + ], + 'external-wiki' => [ + { + required: true, + name: :external_wiki_url, + type: String, + desc: 'The URL of the external Wiki' + } + ], + 'flowdock' => [ + { + required: true, + name: :token, + type: String, + desc: 'Flowdock token' + } + ], + 'gemnasium' => [ + { + required: true, + name: :api_key, + type: String, + desc: 'Your personal API key on gemnasium.com' + }, + { + required: true, + name: :token, + type: String, + desc: "The project's slug on gemnasium.com" + } + ], + 'hipchat' => [ + { + required: true, + name: :token, + type: String, + desc: 'The room token' + }, + { + required: false, + name: :room, + type: String, + desc: 'The room name or ID' + }, + { + required: false, + name: :color, + type: String, + desc: 'The room color' + }, + { + required: false, + name: :notify, + type: Boolean, + desc: 'Enable notifications' + }, + { + required: false, + name: :api_version, + type: String, + desc: 'Leave blank for default (v2)' + }, + { + required: false, + name: :server, + type: String, + desc: 'Leave blank for default. https://hipchat.example.com' + } + ], + 'irker' => [ + { + required: true, + name: :recipients, + type: String, + desc: 'Recipients/channels separated by whitespaces' + }, + { + required: false, + name: :default_irc_uri, + type: String, + desc: 'Default: irc://irc.network.net:6697' + }, + { + required: false, + name: :server_host, + type: String, + desc: 'Server host. Default localhost' + }, + { + required: false, + name: :server_port, + type: Integer, + desc: 'Server port. Default 6659' + }, + { + required: false, + name: :colorize_messages, + type: Boolean, + desc: 'Colorize messages' + } + ], + 'jira' => [ + { + required: true, + name: :url, + type: String, + desc: 'The URL to the JIRA project which is being linked to this GitLab project, e.g., https://jira.example.com' + }, + { + required: true, + name: :project_key, + type: String, + desc: 'The short identifier for your JIRA project, all uppercase, e.g., PROJ' + }, + { + required: false, + name: :username, + type: String, + desc: 'The username of the user created to be used with GitLab/JIRA' + }, + { + required: false, + name: :password, + type: String, + desc: 'The password of the user created to be used with GitLab/JIRA' + }, + { + required: false, + name: :jira_issue_transition_id, + type: Integer, + desc: 'The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`' + } + ], + 'mattermost-slash-commands' => [ + { + required: true, + name: :token, + type: String, + desc: 'The Mattermost token' + } + ], + 'pipelines-email' => [ + { + required: true, + name: :recipients, + type: String, + desc: 'Comma-separated list of recipient email addresses' + }, + { + required: false, + name: :notify_only_broken_builds, + type: Boolean, + desc: 'Notify only broken builds' + } + ], + 'pivotaltracker' => [ + { + required: true, + name: :token, + type: String, + desc: 'The Pivotaltracker token' + }, + { + required: false, + name: :restrict_to_branch, + type: String, + desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.' + } + ], + 'pushover' => [ + { + required: true, + name: :api_key, + type: String, + desc: 'The application key' + }, + { + required: true, + name: :user_key, + type: String, + desc: 'The user key' + }, + { + required: true, + name: :priority, + type: String, + desc: 'The priority' + }, + { + required: true, + name: :device, + type: String, + desc: 'Leave blank for all active devices' + }, + { + required: true, + name: :sound, + type: String, + desc: 'The sound of the notification' + } + ], + 'redmine' => [ + { + required: true, + name: :new_issue_url, + type: String, + desc: 'The new issue URL' + }, + { + required: true, + name: :project_url, + type: String, + desc: 'The project URL' + }, + { + required: true, + name: :issues_url, + type: String, + desc: 'The issues URL' + }, + { + required: false, + name: :description, + type: String, + desc: 'The description of the tracker' + } + ], + 'slack' => [ + { + required: true, + name: :webhook, + type: String, + desc: 'The Slack webhook. e.g. https://hooks.slack.com/services/...' + }, + { + required: false, + name: :new_issue_url, + type: String, + desc: 'The user name' + }, + { + required: false, + name: :channel, + type: String, + desc: 'The channel name' + } + ], + 'teamcity' => [ + { + required: true, + name: :teamcity_url, + type: String, + desc: 'TeamCity root URL like https://teamcity.example.com' + }, + { + required: true, + name: :build_type, + type: String, + desc: 'Build configuration ID' + }, + { + required: true, + name: :username, + type: String, + desc: 'A user with permissions to trigger a manual build' + }, + { + required: true, + name: :password, + type: String, + desc: 'The password of the user' + } + ] + }.freeze + + trigger_services = { + 'mattermost-slash-commands' => [ + { + name: :token, + type: String, + desc: 'The Mattermost token' + } + ] + }.freeze + resource :projects do before { authenticate! } before { authorize_admin_project } - # Set service for project - # - # Example Request: - # - # PUT /projects/:id/services/gitlab-ci - # - put ':id/services/:service_slug' do - if project_service - validators = project_service.class.validators.select do |s| - s.class == ActiveRecord::Validations::PresenceValidator && - s.attributes != [:project_id] + helpers do + def service_attributes(service) + service.fields.inject([]) do |arr, hash| + arr << hash[:name].to_sym end + end + end - required_attributes! validators.map(&:attributes).flatten.uniq - attrs = attributes_for_keys service_attributes + services.each do |service_slug, settings| + desc "Set #{service_slug} service for project" + params do + settings.each do |setting| + if setting[:required] + requires setting[:name], type: setting[:type], desc: setting[:desc] + else + optional setting[:name], type: setting[:type], desc: setting[:desc] + end + end + end + put ":id/services/#{service_slug}" do + service = user_project.find_or_initialize_service(service_slug.underscore) + service_params = declared_params(include_missing: false).merge(active: true) - if project_service.update_attributes(attrs.merge(active: true)) + if service.update_attributes(service_params) true else - not_found! + render_api_error!('400 Bad Request', 400) end end end - # Delete service for project - # - # Example Request: - # - # DELETE /project/:id/services/gitlab-ci - # - delete ':id/services/:service_slug' do - if project_service - attrs = service_attributes.inject({}) do |hash, key| - hash.merge!(key => nil) - end + desc "Delete a service for project" + params do + requires :service_slug, type: String, values: services.keys, desc: 'The name of the service' + end + delete ":id/services/:service_slug" do + service = user_project.find_or_initialize_service(params[:service_slug].underscore) - if project_service.update_attributes(attrs.merge(active: false)) - true - else - not_found! - end + attrs = service_attributes(service).inject({}) do |hash, key| + hash.merge!(key => nil) + end + + if service.update_attributes(attrs.merge(active: false)) + true + else + render_api_error!('400 Bad Request', 400) end end - # Get service settings for project - # - # Example Request: - # - # GET /project/:id/services/gitlab-ci - # - get ':id/services/:service_slug' do - present project_service, with: Entities::ProjectService, include_passwords: current_user.is_admin? + desc 'Get the service settings for project' do + success Entities::ProjectService + end + params do + requires :service_slug, type: String, values: services.keys, desc: 'The name of the service' + end + get ":id/services/:service_slug" do + service = user_project.find_or_initialize_service(params[:service_slug].underscore) + present service, with: Entities::ProjectService, include_passwords: current_user.is_admin? end end - resource :projects do - desc 'Trigger a slash command' do - detail 'Added in GitLab 8.13' + trigger_services.each do |service_slug, settings| + params do + requires :id, type: String, desc: 'The ID of a project' end - post ':id/services/:service_slug/trigger' do - project = find_project(params[:id]) + resource :projects do + desc "Trigger a slash command for #{service_slug}" do + detail 'Added in GitLab 8.13' + end + params do + settings.each do |setting| + requires setting[:name], type: setting[:type], desc: setting[:desc] + end + end + post ":id/services/#{service_slug.underscore}/trigger" do + project = find_project(params[:id]) - # This is not accurate, but done to prevent leakage of the project names - not_found!('Service') unless project + # This is not accurate, but done to prevent leakage of the project names + not_found!('Service') unless project - service = project_service(project) + service = project.find_or_initialize_service(service_slug.underscore) - result = service.try(:active?) && service.try(:trigger, params) + result = service.try(:active?) && service.try(:trigger, params) - if result - status result[:status] || 200 - present result - else - not_found!('Service') + if result + status result[:status] || 200 + present result + else + not_found!('Service') + end end end end diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index 2da3a9cb09f446..564e49d5459ed6 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -358,7 +358,7 @@ context 'with a failed build' do it 'uses the red color' do build_data = { object_kind: 'build', commit: { status: 'failed' } } - + expect(hipchat.__send__(:message_options, build_data)).to eq({ notify: false, color: 'red' }) end end diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb index d30361f53d4aa0..668e39f9dba983 100644 --- a/spec/requests/api/services_spec.rb +++ b/spec/requests/api/services_spec.rb @@ -2,6 +2,7 @@ describe API::Services, api: true do include ApiHelpers + let(:user) { create(:user) } let(:admin) { create(:admin) } let(:user2) { create(:user) } @@ -98,7 +99,7 @@ post api("/projects/#{project.id}/services/idonotexist/trigger") expect(response).to have_http_status(404) - expect(json_response["message"]).to eq("404 Service Not Found") + expect(json_response["error"]).to eq("404 Not Found") end end @@ -114,7 +115,7 @@ end it 'when the service is inactive' do - post api("/projects/#{project.id}/services/mattermost_slash_commands/trigger") + post api("/projects/#{project.id}/services/mattermost_slash_commands/trigger"), params expect(response).to have_http_status(404) end diff --git a/spec/support/services_shared_context.rb b/spec/support/services_shared_context.rb index d1c999cad4de8d..66c93890e31ef2 100644 --- a/spec/support/services_shared_context.rb +++ b/spec/support/services_shared_context.rb @@ -16,8 +16,14 @@ hash.merge!(k => 'secrettoken') elsif k =~ /^(.*_url|url|webhook)/ hash.merge!(k => "http://example.com") + elsif service_klass.method_defined?("#{k}?") + hash.merge!(k => true) elsif service == 'irker' && k == :recipients hash.merge!(k => 'irc://irc.network.net:666/#channel') + elsif service == 'irker' && k == :server_port + hash.merge!(k => 1234) + elsif service == 'jira' && k == :jira_issue_transition_id + hash.merge!(k => 1234) else hash.merge!(k => "someword") end -- GitLab From 5934698fc3b7aaf6623959a9740e62ed3d341a42 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 3 Oct 2016 16:14:17 -0500 Subject: [PATCH 003/191] fix broken string interpolation --- app/views/projects/commit/_change.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml index f6e3d5e76f5df4..59af45ee1ed3bb 100644 --- a/app/views/projects/commit/_change.html.haml +++ b/app/views/projects/commit/_change.html.haml @@ -13,7 +13,7 @@ %a.close{href: "#", "data-dismiss" => "modal"} × %h3.page-title== #{label} this #{commit.change_type_title(current_user)} .modal-body - = form_tag send("#{type.underscore}_namespace_project_commit_path", @project.namespace, @project, commit.id), method: :post, remote: false, class: 'form-horizontal js-#{type}-form js-requires-input' do + = form_tag send("#{type.underscore}_namespace_project_commit_path", @project.namespace, @project, commit.id), method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do .form-group.branch = label_tag 'target_branch', target_label, class: 'control-label' .col-sm-10 -- GitLab From 7876e83d0c91e75b91e21ddb9993fc39fde96731 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 3 Oct 2016 17:04:23 -0500 Subject: [PATCH 004/191] remove unnecessary nonce id --- app/views/projects/commit/_change.html.haml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml index 59af45ee1ed3bb..e38ee6a2afa2b8 100644 --- a/app/views/projects/commit/_change.html.haml +++ b/app/views/projects/commit/_change.html.haml @@ -23,9 +23,8 @@ - if can?(current_user, :push_code, @project) .js-create-merge-request-container .checkbox - - nonce = SecureRandom.hex - = label_tag "create_merge_request-#{nonce}" do - = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: "create_merge_request-#{nonce}" + = label_tag do + = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request' Start a new merge request with these changes - else = hidden_field_tag 'create_merge_request', 1 -- GitLab From 79aad815272fdebca30080cb33dd3fa77ef72350 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 5 Oct 2016 10:12:46 -0500 Subject: [PATCH 005/191] fix awkward verb conjugation in cherry-pick and revert errors --- app/services/commits/change_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb index db5f2bf9b2e5dd..4d410f66c55c72 100644 --- a/app/services/commits/change_service.rb +++ b/app/services/commits/change_service.rb @@ -35,7 +35,7 @@ def commit_change(action) success else error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically. - It may have already been #{action.to_s.dasherize}, or a more recent commit may have updated some of its content." + A #{action.to_s.dasherize} may have already been performed with this #{@commit.change_type_title(current_user)}, or a more recent commit may have updated some of its content." raise ChangeError, error_msg end end -- GitLab From 130cbc979aff4ce746cabc7d319a34904b1fd611 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Sat, 10 Dec 2016 00:44:04 -0600 Subject: [PATCH 006/191] prevent create_merge_request form field helpers from generating an id value --- app/views/projects/commit/_change.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml index e38ee6a2afa2b8..782f558e8b081c 100644 --- a/app/views/projects/commit/_change.html.haml +++ b/app/views/projects/commit/_change.html.haml @@ -24,10 +24,10 @@ .js-create-merge-request-container .checkbox = label_tag do - = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request' + = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: nil Start a new merge request with these changes - else - = hidden_field_tag 'create_merge_request', 1 + = hidden_field_tag 'create_merge_request', 1, id: nil .form-actions = submit_tag label, class: 'btn btn-create' = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" -- GitLab From 758b3055c5b25e2b5dcdb12c169d23303d3bfe0d Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 7 Dec 2016 14:58:27 -0600 Subject: [PATCH 007/191] update action button order for snippets page --- app/views/projects/snippets/_actions.html.haml | 12 ++++++------ app/views/snippets/_actions.html.haml | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml index 32e1f8a21b004a..346badb8f25ceb 100644 --- a/app/views/projects/snippets/_actions.html.haml +++ b/app/views/projects/snippets/_actions.html.haml @@ -1,13 +1,13 @@ .hidden-xs - - if can?(current_user, :create_project_snippet, @project) - = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create new-snippet-link', title: "New snippet" do - New snippet - - if can?(current_user, :update_project_snippet, @snippet) - = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do - Delete - if can?(current_user, :update_project_snippet, @snippet) = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped snippable-edit" do Edit + - if can?(current_user, :update_project_snippet, @snippet) + = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do + Delete + - if can?(current_user, :create_project_snippet, @project) + = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create new-snippet-link', title: "New snippet" do + New snippet - if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet) .visible-xs-block.dropdown %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml index 1d0e549ed3d64e..2ed5894d312376 100644 --- a/app/views/snippets/_actions.html.haml +++ b/app/views/snippets/_actions.html.haml @@ -1,13 +1,13 @@ .hidden-xs - - if current_user - = link_to new_snippet_path, class: "btn btn-grouped btn-create new-snippet-link", title: "New snippet" do - New snippet - - if can?(current_user, :admin_personal_snippet, @snippet) - = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do - Delete - if can?(current_user, :update_personal_snippet, @snippet) = link_to edit_snippet_path(@snippet), class: "btn btn-grouped snippable-edit" do Edit + - if can?(current_user, :admin_personal_snippet, @snippet) + = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do + Delete + - if current_user + = link_to new_snippet_path, class: "btn btn-grouped btn-create new-snippet-link", title: "New snippet" do + New snippet - if current_user .visible-xs-block.dropdown %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } -- GitLab From 12a8095cc7a30469c4546af1ed801c785b0ebace Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 7 Dec 2016 15:08:19 -0600 Subject: [PATCH 008/191] remove unused class name --- app/views/projects/snippets/_actions.html.haml | 4 ++-- app/views/snippets/_actions.html.haml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml index 346badb8f25ceb..d2a97476957559 100644 --- a/app/views/projects/snippets/_actions.html.haml +++ b/app/views/projects/snippets/_actions.html.haml @@ -1,12 +1,12 @@ .hidden-xs - if can?(current_user, :update_project_snippet, @snippet) - = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped snippable-edit" do + = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped" do Edit - if can?(current_user, :update_project_snippet, @snippet) = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do Delete - if can?(current_user, :create_project_snippet, @project) - = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create new-snippet-link', title: "New snippet" do + = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create', title: "New snippet" do New snippet - if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet) .visible-xs-block.dropdown diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml index 2ed5894d312376..ebb3dfe5a34c47 100644 --- a/app/views/snippets/_actions.html.haml +++ b/app/views/snippets/_actions.html.haml @@ -1,12 +1,12 @@ .hidden-xs - if can?(current_user, :update_personal_snippet, @snippet) - = link_to edit_snippet_path(@snippet), class: "btn btn-grouped snippable-edit" do + = link_to edit_snippet_path(@snippet), class: "btn btn-grouped" do Edit - if can?(current_user, :admin_personal_snippet, @snippet) = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do Delete - if current_user - = link_to new_snippet_path, class: "btn btn-grouped btn-create new-snippet-link", title: "New snippet" do + = link_to new_snippet_path, class: "btn btn-grouped btn-create", title: "New snippet" do New snippet - if current_user .visible-xs-block.dropdown -- GitLab From c41b7e8a2f4df9a3a320cbf1b5b60a43709ca9b6 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 7 Dec 2016 15:22:16 -0600 Subject: [PATCH 009/191] invert snippet action buttons --- app/views/projects/snippets/_actions.html.haml | 4 ++-- app/views/snippets/_actions.html.haml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml index d2a97476957559..068a66103501e1 100644 --- a/app/views/projects/snippets/_actions.html.haml +++ b/app/views/projects/snippets/_actions.html.haml @@ -3,10 +3,10 @@ = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped" do Edit - if can?(current_user, :update_project_snippet, @snippet) - = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do + = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-inverted btn-remove", title: 'Delete Snippet' do Delete - if can?(current_user, :create_project_snippet, @project) - = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create', title: "New snippet" do + = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-inverted btn-create', title: "New snippet" do New snippet - if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet) .visible-xs-block.dropdown diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml index ebb3dfe5a34c47..95fc71981044a0 100644 --- a/app/views/snippets/_actions.html.haml +++ b/app/views/snippets/_actions.html.haml @@ -3,10 +3,10 @@ = link_to edit_snippet_path(@snippet), class: "btn btn-grouped" do Edit - if can?(current_user, :admin_personal_snippet, @snippet) - = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do + = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-inverted btn-remove", title: 'Delete Snippet' do Delete - if current_user - = link_to new_snippet_path, class: "btn btn-grouped btn-create", title: "New snippet" do + = link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-create", title: "New snippet" do New snippet - if current_user .visible-xs-block.dropdown -- GitLab From 6b20ad3646694ae90e8375f92cb5df13e2fd9fad Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 7 Dec 2016 15:41:39 -0600 Subject: [PATCH 010/191] remove plus icon in "new snippet" button --- app/views/dashboard/snippets/index.html.haml | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml index b2af438ea57670..62618625454116 100644 --- a/app/views/dashboard/snippets/index.html.haml +++ b/app/views/dashboard/snippets/index.html.haml @@ -6,7 +6,6 @@ .nav-block .controls.hidden-xs = link_to new_snippet_path, class: "btn btn-new", title: "New snippet" do - = icon('plus') New snippet .nav-links.snippet-scope-menu @@ -36,7 +35,6 @@ .visible-xs = link_to new_snippet_path, class: "btn btn-new btn-block", title: "New snippet" do - = icon('plus') New snippet = render 'snippets/snippets' -- GitLab From b65d3e1132762b9e1a39ce908b3b481f92d9a10c Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 7 Dec 2016 16:03:00 -0600 Subject: [PATCH 011/191] move new snippet button to main snippet navigation block --- app/views/dashboard/_snippets_head.html.haml | 20 ++++--- app/views/dashboard/snippets/index.html.haml | 62 +++++++++----------- app/views/explore/snippets/index.html.haml | 8 --- 3 files changed, 42 insertions(+), 48 deletions(-) diff --git a/app/views/dashboard/_snippets_head.html.haml b/app/views/dashboard/_snippets_head.html.haml index b25e8ea1f0cdfb..02e90bbfa55336 100644 --- a/app/views/dashboard/_snippets_head.html.haml +++ b/app/views/dashboard/_snippets_head.html.haml @@ -1,7 +1,13 @@ -%ul.nav-links - = nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do - = link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do - Your Snippets - = nav_link(page: explore_snippets_path) do - = link_to explore_snippets_path, title: 'Explore snippets', data: {placement: 'right'} do - Explore Snippets +.top-area + %ul.nav-links + = nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do + = link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do + Your Snippets + = nav_link(page: explore_snippets_path) do + = link_to explore_snippets_path, title: 'Explore snippets', data: {placement: 'right'} do + Explore Snippets + + - if current_user + .nav-controls.hidden-xs + = link_to new_snippet_path, class: "btn btn-new", title: "New snippet" do + New snippet diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml index 62618625454116..13cba7ca22414e 100644 --- a/app/views/dashboard/snippets/index.html.haml +++ b/app/views/dashboard/snippets/index.html.haml @@ -3,38 +3,34 @@ = render 'dashboard/snippets_head' -.nav-block - .controls.hidden-xs - = link_to new_snippet_path, class: "btn btn-new", title: "New snippet" do - New snippet - - .nav-links.snippet-scope-menu - %li{ class: ("active" unless params[:scope]) } - = link_to dashboard_snippets_path do - All - %span.badge - = current_user.snippets.count - - %li{ class: ("active" if params[:scope] == "are_private") } - = link_to dashboard_snippets_path(scope: 'are_private') do - Private - %span.badge - = current_user.snippets.are_private.count - - %li{ class: ("active" if params[:scope] == "are_internal") } - = link_to dashboard_snippets_path(scope: 'are_internal') do - Internal - %span.badge - = current_user.snippets.are_internal.count - - %li{ class: ("active" if params[:scope] == "are_public") } - = link_to dashboard_snippets_path(scope: 'are_public') do - Public - %span.badge - = current_user.snippets.are_public.count - - .visible-xs - = link_to new_snippet_path, class: "btn btn-new btn-block", title: "New snippet" do - New snippet +.nav-links.snippet-scope-menu + %li{ class: ("active" unless params[:scope]) } + = link_to dashboard_snippets_path do + All + %span.badge + = current_user.snippets.count + + %li{ class: ("active" if params[:scope] == "are_private") } + = link_to dashboard_snippets_path(scope: 'are_private') do + Private + %span.badge + = current_user.snippets.are_private.count + + %li{ class: ("active" if params[:scope] == "are_internal") } + = link_to dashboard_snippets_path(scope: 'are_internal') do + Internal + %span.badge + = current_user.snippets.are_internal.count + + %li{ class: ("active" if params[:scope] == "are_public") } + = link_to dashboard_snippets_path(scope: 'are_public') do + Public + %span.badge + = current_user.snippets.are_public.count + +.visible-xs +   + = link_to new_snippet_path, class: "btn btn-new btn-block", title: "New snippet" do + New snippet = render 'snippets/snippets' diff --git a/app/views/explore/snippets/index.html.haml b/app/views/explore/snippets/index.html.haml index 7def9eacdc9f3e..9b5ea13ca29ea8 100644 --- a/app/views/explore/snippets/index.html.haml +++ b/app/views/explore/snippets/index.html.haml @@ -6,12 +6,4 @@ - else = render 'explore/head' -.row-content-block - - if current_user - = link_to new_snippet_path, class: "btn btn-new btn-wide-on-sm pull-right", title: "New snippet" do - New snippet - - .oneline - Public snippets created by you and other users are listed here - = render 'snippets/snippets' -- GitLab From 54a1193d790ae40fea5db1d8596c12fbc7a93576 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 7 Dec 2016 16:35:53 -0600 Subject: [PATCH 012/191] add scope filters to project snippets page --- .../projects/snippets_controller.rb | 8 +++--- app/finders/snippets_finder.rb | 24 ++++++++++++++--- app/views/projects/snippets/index.html.haml | 27 +++++++++++++++++++ 3 files changed, 52 insertions(+), 7 deletions(-) diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index e290a0eadda814..0720be2e55d15e 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -19,10 +19,12 @@ class Projects::SnippetsController < Projects::ApplicationController respond_to :html def index - @snippets = SnippetsFinder.new.execute(current_user, { + @snippets = SnippetsFinder.new.execute( + current_user, filter: :by_project, - project: @project - }) + project: @project, + scope: params[:scope] + ) @snippets = @snippets.page(params[:page]) end diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb index 00ff161103932e..99f1e73c8005d7 100644 --- a/app/finders/snippets_finder.rb +++ b/app/finders/snippets_finder.rb @@ -8,7 +8,7 @@ def execute(current_user, params = {}) when :by_user then by_user(current_user, params[:user], params[:scope]) when :by_project - by_project(current_user, params[:project]) + by_project(current_user, params[:project], params[:scope]) end end @@ -47,14 +47,30 @@ def by_user(current_user, user, scope) end end - def by_project(current_user, project) + def by_project(current_user, project, scope) snippets = project.snippets.fresh if current_user if project.team.member?(current_user) || current_user.admin? - snippets + case scope + when 'are_internal' then + snippets.are_internal + when 'are_private' then + snippets.are_private + when 'are_public' then + snippets.are_public + else + snippets + end else - snippets.public_and_internal + case scope + when 'are_internal' then + snippets.are_internal + when 'are_public' then + snippets.are_public + else + snippets.public_and_internal + end end else snippets.are_public diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml index e77e1b026f6ec2..76792fb53267dd 100644 --- a/app/views/projects/snippets/index.html.haml +++ b/app/views/projects/snippets/index.html.haml @@ -1,5 +1,32 @@ - page_title "Snippets" +- if current_user + .nav-links.snippet-scope-menu + %li{ class: ("active" unless params[:scope]) } + = link_to namespace_project_snippets_path(@project.namespace, @project) do + All + %span.badge + = @project.snippets.count + + - if @project.team.member?(current_user) || current_user.admin? + %li{ class: ("active" if params[:scope] == "are_private") } + = link_to namespace_project_snippets_path(@project.namespace, @project, scope: 'are_private') do + Private + %span.badge + = @project.snippets.are_private.count + + %li{ class: ("active" if params[:scope] == "are_internal") } + = link_to namespace_project_snippets_path(@project.namespace, @project, scope: 'are_internal') do + Internal + %span.badge + = @project.snippets.are_internal.count + + %li{ class: ("active" if params[:scope] == "are_public") } + = link_to namespace_project_snippets_path(@project.namespace, @project, scope: 'are_public') do + Public + %span.badge + = @project.snippets.are_public.count + .sub-header-block - if can?(current_user, :create_project_snippet, @project) = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new btn-wide-on-sm pull-right", title: "New snippet" do -- GitLab From 68bb459b160419004ef2110b2824c8b2ab4c9739 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 7 Dec 2016 16:48:26 -0600 Subject: [PATCH 013/191] move project new snippet button into snippet scope navigation header --- app/views/projects/snippets/index.html.haml | 58 +++++++++++---------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml index 76792fb53267dd..35c4e9d85ad8f2 100644 --- a/app/views/projects/snippets/index.html.haml +++ b/app/views/projects/snippets/index.html.haml @@ -1,38 +1,42 @@ - page_title "Snippets" - if current_user - .nav-links.snippet-scope-menu - %li{ class: ("active" unless params[:scope]) } - = link_to namespace_project_snippets_path(@project.namespace, @project) do - All - %span.badge - = @project.snippets.count + .top-area + .nav-links.snippet-scope-menu + %li{ class: ("active" unless params[:scope]) } + = link_to namespace_project_snippets_path(@project.namespace, @project) do + All + %span.badge + = @project.snippets.count + + - if @project.team.member?(current_user) || current_user.admin? + %li{ class: ("active" if params[:scope] == "are_private") } + = link_to namespace_project_snippets_path(@project.namespace, @project, scope: 'are_private') do + Private + %span.badge + = @project.snippets.are_private.count - - if @project.team.member?(current_user) || current_user.admin? - %li{ class: ("active" if params[:scope] == "are_private") } - = link_to namespace_project_snippets_path(@project.namespace, @project, scope: 'are_private') do - Private + %li{ class: ("active" if params[:scope] == "are_internal") } + = link_to namespace_project_snippets_path(@project.namespace, @project, scope: 'are_internal') do + Internal %span.badge - = @project.snippets.are_private.count + = @project.snippets.are_internal.count - %li{ class: ("active" if params[:scope] == "are_internal") } - = link_to namespace_project_snippets_path(@project.namespace, @project, scope: 'are_internal') do - Internal - %span.badge - = @project.snippets.are_internal.count + %li{ class: ("active" if params[:scope] == "are_public") } + = link_to namespace_project_snippets_path(@project.namespace, @project, scope: 'are_public') do + Public + %span.badge + = @project.snippets.are_public.count - %li{ class: ("active" if params[:scope] == "are_public") } - = link_to namespace_project_snippets_path(@project.namespace, @project, scope: 'are_public') do - Public - %span.badge - = @project.snippets.are_public.count + .nav-controls.hidden-xs + - if can?(current_user, :create_project_snippet, @project) + = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New snippet" do + New snippet -.sub-header-block - - if can?(current_user, :create_project_snippet, @project) - = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new btn-wide-on-sm pull-right", title: "New snippet" do +- if can?(current_user, :create_project_snippet, @project) + .visible-xs +   + = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new btn-block", title: "New snippet" do New snippet - .oneline - Share code pastes with others out of git repository - = render 'snippets/snippets' -- GitLab From 1ea478476404d6696d65269fd2ffe6ca29740035 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 7 Dec 2016 16:49:06 -0600 Subject: [PATCH 014/191] ensure all snippets count badge is accurate for non team members --- app/views/projects/snippets/index.html.haml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml index 35c4e9d85ad8f2..978f4b87564e15 100644 --- a/app/views/projects/snippets/index.html.haml +++ b/app/views/projects/snippets/index.html.haml @@ -7,7 +7,10 @@ = link_to namespace_project_snippets_path(@project.namespace, @project) do All %span.badge - = @project.snippets.count + - if @project.team.member?(current_user) || current_user.admin? + = @project.snippets.count + - else + = @project.snippets.public_and_internal.count - if @project.team.member?(current_user) || current_user.admin? %li{ class: ("active" if params[:scope] == "are_private") } -- GitLab From 68c1e3a1568d9f61bf1e01d6ff55ce59c5c3eaaf Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 8 Dec 2016 14:29:15 -0600 Subject: [PATCH 015/191] update snippets list design --- app/assets/stylesheets/pages/snippets.scss | 10 +++++++++ app/views/shared/snippets/_snippet.html.haml | 23 +++++++++----------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss index 857eb76131a969..e6e86556695d61 100644 --- a/app/assets/stylesheets/pages/snippets.scss +++ b/app/assets/stylesheets/pages/snippets.scss @@ -1,3 +1,13 @@ +.snippet-row { + .title { + margin-bottom: 2px; + } + + .snippet-filename { + padding: 0 2px; + } +} + .snippet-form-holder .file-holder .file-title { padding: 2px; } diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml index ea17bec8677ed4..95985ad6afb16e 100644 --- a/app/views/shared/snippets/_snippet.html.haml +++ b/app/views/shared/snippets/_snippet.html.haml @@ -4,14 +4,11 @@ .title = link_to reliable_snippet_path(snippet) do = snippet.title - - if snippet.private? - %span.label.label-gray.hidden-xs - = icon('lock') - private - %span.monospace.pull-right.hidden-xs - = snippet.file_name + - if snippet.file_name + %span.snippet-filename.monospace.hidden-xs + = snippet.file_name - %ul.controls.visible-xs + %ul.controls %li - note_count = snippet.notes.user.count = link_to reliable_snippet_path(snippet, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do @@ -22,11 +19,11 @@ = visibility_level_label(snippet.visibility_level) = visibility_level_icon(snippet.visibility_level, fw: false) - %small.pull-right.cgray.hidden-xs - - if snippet.project_id? - = link_to snippet.project.name_with_namespace, namespace_project_path(snippet.project.namespace, snippet.project) - - .snippet-info.hidden-xs + .snippet-info + #{snippet.to_reference} · + authored #{time_ago_with_tooltip(snippet.created_at, placement: 'bottom')} by = link_to user_snippets_path(snippet.author) do = snippet.author_name - authored #{time_ago_with_tooltip(snippet.created_at)} + + .pull-right.snippet-updated-at + %span updated #{time_ago_with_tooltip(snippet.updated_at, placement: 'bottom', html_class: 'snippet_update_ago')} -- GitLab From 7f3fc26ec98193fa0c3bfeb7b78c81176bb9c689 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 8 Dec 2016 16:13:23 -0600 Subject: [PATCH 016/191] fix failing tests --- app/views/shared/snippets/_snippet.html.haml | 4 ++-- features/steps/project/snippets.rb | 2 +- spec/features/dashboard/datetime_on_tooltips_spec.rb | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml index 95985ad6afb16e..659469105298d7 100644 --- a/app/views/shared/snippets/_snippet.html.haml +++ b/app/views/shared/snippets/_snippet.html.haml @@ -21,9 +21,9 @@ .snippet-info #{snippet.to_reference} · - authored #{time_ago_with_tooltip(snippet.created_at, placement: 'bottom')} by + authored #{time_ago_with_tooltip(snippet.created_at, placement: 'bottom', html_class: 'snippet-created-ago')} by = link_to user_snippets_path(snippet.author) do = snippet.author_name .pull-right.snippet-updated-at - %span updated #{time_ago_with_tooltip(snippet.updated_at, placement: 'bottom', html_class: 'snippet_update_ago')} + %span updated #{time_ago_with_tooltip(snippet.updated_at, placement: 'bottom')} diff --git a/features/steps/project/snippets.rb b/features/steps/project/snippets.rb index 5e7d539add6c18..a3bebfa4b71505 100644 --- a/features/steps/project/snippets.rb +++ b/features/steps/project/snippets.rb @@ -22,7 +22,7 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps end step 'I click link "New snippet"' do - click_link "New snippet" + first(:link, "New snippet").click end step 'I click link "Snippet one"' do diff --git a/spec/features/dashboard/datetime_on_tooltips_spec.rb b/spec/features/dashboard/datetime_on_tooltips_spec.rb index 365cb445df1e51..44dfc2dff450b5 100644 --- a/spec/features/dashboard/datetime_on_tooltips_spec.rb +++ b/spec/features/dashboard/datetime_on_tooltips_spec.rb @@ -36,7 +36,7 @@ visit user_snippets_path(user) wait_for_ajax() - page.find('.js-timeago').hover + page.find('.js-timeago.snippet-created-ago').hover end it 'has the datetime formated correctly' do -- GitLab From 0608ecbc69c991efcf56f7872cf1b06416d4bd10 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 9 Dec 2016 10:54:13 -0600 Subject: [PATCH 017/191] conditionally display assoc project info in snippets index --- app/views/dashboard/snippets/index.html.haml | 2 +- app/views/explore/snippets/index.html.haml | 2 +- app/views/shared/snippets/_snippet.html.haml | 10 +++++++++- app/views/snippets/_snippets.html.haml | 3 ++- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml index 13cba7ca22414e..81bfa44a6650b3 100644 --- a/app/views/dashboard/snippets/index.html.haml +++ b/app/views/dashboard/snippets/index.html.haml @@ -33,4 +33,4 @@ = link_to new_snippet_path, class: "btn btn-new btn-block", title: "New snippet" do New snippet -= render 'snippets/snippets' += render partial: 'snippets/snippets', locals: { link_project: true } diff --git a/app/views/explore/snippets/index.html.haml b/app/views/explore/snippets/index.html.haml index 9b5ea13ca29ea8..e5706d04736287 100644 --- a/app/views/explore/snippets/index.html.haml +++ b/app/views/explore/snippets/index.html.haml @@ -6,4 +6,4 @@ - else = render 'explore/head' -= render 'snippets/snippets' += render partial: 'snippets/snippets', locals: { link_project: true } diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml index 659469105298d7..5d2d2317f22697 100644 --- a/app/views/shared/snippets/_snippet.html.haml +++ b/app/views/shared/snippets/_snippet.html.haml @@ -1,3 +1,5 @@ +- link_project = local_assigns.fetch(:link_project, false) + %li.snippet-row = image_tag avatar_icon(snippet.author_email), class: "avatar s40 hidden-xs", alt: '' @@ -21,9 +23,15 @@ .snippet-info #{snippet.to_reference} · - authored #{time_ago_with_tooltip(snippet.created_at, placement: 'bottom', html_class: 'snippet-created-ago')} by + authored #{time_ago_with_tooltip(snippet.created_at, placement: 'bottom', html_class: 'snippet-created-ago')} + by = link_to user_snippets_path(snippet.author) do = snippet.author_name + - if link_project && snippet.project_id? + %span.hidden-xs + in + = link_to namespace_project_path(snippet.project.namespace, snippet.project) do + = snippet.project.name_with_namespace .pull-right.snippet-updated-at %span updated #{time_ago_with_tooltip(snippet.updated_at, placement: 'bottom')} diff --git a/app/views/snippets/_snippets.html.haml b/app/views/snippets/_snippets.html.haml index 77b66ca74b6385..ac3701233ad1e8 100644 --- a/app/views/snippets/_snippets.html.haml +++ b/app/views/snippets/_snippets.html.haml @@ -1,8 +1,9 @@ - remote = local_assigns.fetch(:remote, false) +- link_project = local_assigns.fetch(:link_project, false) .snippets-list-holder %ul.content-list - = render partial: 'shared/snippets/snippet', collection: @snippets + = render partial: 'shared/snippets/snippet', collection: @snippets, locals: { link_project: link_project } - if @snippets.empty? %li .nothing-here-block Nothing here. -- GitLab From 6dc4007ad6c775a8d82bb1e3e2e15ec6e8143b14 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 9 Dec 2016 11:45:40 -0600 Subject: [PATCH 018/191] fix snippets reference id in search results (should be $ not #) --- app/views/search/results/_snippet_title.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml index c414acb6a11118..027d42396b48f4 100644 --- a/app/views/search/results/_snippet_title.html.haml +++ b/app/views/search/results/_snippet_title.html.haml @@ -14,7 +14,7 @@ = link_to snippet_title.project.name_with_namespace, namespace_project_path(snippet_title.project.namespace, snippet_title.project) .snippet-info - = "##{snippet_title.id}" + = snippet_title.to_reference %span by = link_to user_snippets_path(snippet_title.author) do -- GitLab From eaf92daa2ce2601426c991c794aab57c4d0da420 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 9 Dec 2016 11:48:53 -0600 Subject: [PATCH 019/191] move snippet edited timeago under the snippet title --- app/assets/stylesheets/pages/snippets.scss | 10 ++++++++-- app/views/shared/snippets/_header.html.haml | 12 ++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss index e6e86556695d61..ff13b86acf0da3 100644 --- a/app/assets/stylesheets/pages/snippets.scss +++ b/app/assets/stylesheets/pages/snippets.scss @@ -34,11 +34,17 @@ padding-bottom: $gl-padding; } +.snippet-header { + padding: $gl-padding 0; +} + .snippet-title { font-size: 24px; font-weight: 600; - padding: $gl-padding; - padding-left: 0; +} + +.snippet-edited-ago { + color: $gray-darkest; } .snippet-actions { diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml index d7506e07ff6b06..d084f5e9684167 100644 --- a/app/views/shared/snippets/_header.html.haml +++ b/app/views/shared/snippets/_header.html.haml @@ -8,10 +8,6 @@ %span.creator authored = time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago') - - if @snippet.updated_at != @snippet.created_at - %span - = icon('edit', title: 'edited') - = time_ago_with_tooltip(@snippet.updated_at, placement: 'bottom', html_class: 'snippet_edited_ago') by #{link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title", avatar_class: "hidden-xs")} .snippet-actions @@ -20,5 +16,9 @@ - else = render "snippets/actions" -%h2.snippet-title.prepend-top-0.append-bottom-0 - = markdown_field(@snippet, :title) +.snippet-header + %h2.snippet-title.prepend-top-0.append-bottom-0 + = markdown_field(@snippet, :title) + + - if @snippet.updated_at != @snippet.created_at + = edited_time_ago_with_tooltip(@snippet, placement: 'bottom', html_class: 'snippet-edited-ago') -- GitLab From adbc37804e49e1d3ba02bf61122696e135666ff3 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 9 Dec 2016 14:40:48 -0600 Subject: [PATCH 020/191] refactor duplicate code into a by_scope method --- app/finders/snippets_finder.rb | 54 ++++++++++++---------------------- 1 file changed, 19 insertions(+), 35 deletions(-) diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb index 99f1e73c8005d7..31f039b5a70e97 100644 --- a/app/finders/snippets_finder.rb +++ b/app/finders/snippets_finder.rb @@ -29,21 +29,11 @@ def snippets(current_user) def by_user(current_user, user, scope) snippets = user.snippets.fresh - return snippets.are_public unless current_user - - if user == current_user - case scope - when 'are_internal' then - snippets.are_internal - when 'are_private' then - snippets.are_private - when 'are_public' then - snippets.are_public - else - snippets - end + if current_user + include_private = user == current_user + by_scope(snippets, scope, include_private) else - snippets.public_and_internal + snippets.are_public end end @@ -51,29 +41,23 @@ def by_project(current_user, project, scope) snippets = project.snippets.fresh if current_user - if project.team.member?(current_user) || current_user.admin? - case scope - when 'are_internal' then - snippets.are_internal - when 'are_private' then - snippets.are_private - when 'are_public' then - snippets.are_public - else - snippets - end - else - case scope - when 'are_internal' then - snippets.are_internal - when 'are_public' then - snippets.are_public - else - snippets.public_and_internal - end - end + include_private = project.team.member?(current_user) || current_user.admin? + by_scope(snippets, scope, include_private) else snippets.are_public end end + + def by_scope(snippets, scope = nil, include_private = false) + case scope.to_s + when 'are_private' + include_private ? snippets.are_private : nil + when 'are_internal' + snippets.are_internal + when 'are_public' + snippets.are_public + else + include_private ? snippets : snippets.public_and_internal + end + end end -- GitLab From 687872978100c168ce381448c0a9536fb53542ce Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 9 Dec 2016 15:38:10 -0600 Subject: [PATCH 021/191] implement snippets_scope_menu partial to reduce code duplication --- app/helpers/snippets_helper.rb | 11 +++++++ app/views/dashboard/snippets/index.html.haml | 26 +--------------- app/views/projects/snippets/index.html.haml | 30 ++---------------- .../snippets/_snippets_scope_menu.html.haml | 31 +++++++++++++++++++ 4 files changed, 45 insertions(+), 53 deletions(-) create mode 100644 app/views/snippets/_snippets_scope_menu.html.haml diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb index 7e33a562077564..fc7febd338561b 100644 --- a/app/helpers/snippets_helper.rb +++ b/app/helpers/snippets_helper.rb @@ -8,6 +8,17 @@ def reliable_snippet_path(snippet, opts = nil) end end + # Return the path of a snippets index for a user or for a project + # + # @returns String, path to snippet index + def snippets_path(subject = nil, opts = nil) + if subject.is_a?(Project) + namespace_project_snippets_path(subject.namespace, subject, opts) + else # assume subject === User + dashboard_snippets_path(opts) + end + end + # Get an array of line numbers surrounding a matching # line, bounded by min/max. # diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml index 81bfa44a6650b3..85cbe0bf0e6e7a 100644 --- a/app/views/dashboard/snippets/index.html.haml +++ b/app/views/dashboard/snippets/index.html.haml @@ -2,31 +2,7 @@ - header_title "Snippets", dashboard_snippets_path = render 'dashboard/snippets_head' - -.nav-links.snippet-scope-menu - %li{ class: ("active" unless params[:scope]) } - = link_to dashboard_snippets_path do - All - %span.badge - = current_user.snippets.count - - %li{ class: ("active" if params[:scope] == "are_private") } - = link_to dashboard_snippets_path(scope: 'are_private') do - Private - %span.badge - = current_user.snippets.are_private.count - - %li{ class: ("active" if params[:scope] == "are_internal") } - = link_to dashboard_snippets_path(scope: 'are_internal') do - Internal - %span.badge - = current_user.snippets.are_internal.count - - %li{ class: ("active" if params[:scope] == "are_public") } - = link_to dashboard_snippets_path(scope: 'are_public') do - Public - %span.badge - = current_user.snippets.are_public.count += render partial: 'snippets/snippets_scope_menu', locals: { include_private: true } .visible-xs   diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml index 978f4b87564e15..84e05cd6d88b49 100644 --- a/app/views/projects/snippets/index.html.haml +++ b/app/views/projects/snippets/index.html.haml @@ -2,34 +2,8 @@ - if current_user .top-area - .nav-links.snippet-scope-menu - %li{ class: ("active" unless params[:scope]) } - = link_to namespace_project_snippets_path(@project.namespace, @project) do - All - %span.badge - - if @project.team.member?(current_user) || current_user.admin? - = @project.snippets.count - - else - = @project.snippets.public_and_internal.count - - - if @project.team.member?(current_user) || current_user.admin? - %li{ class: ("active" if params[:scope] == "are_private") } - = link_to namespace_project_snippets_path(@project.namespace, @project, scope: 'are_private') do - Private - %span.badge - = @project.snippets.are_private.count - - %li{ class: ("active" if params[:scope] == "are_internal") } - = link_to namespace_project_snippets_path(@project.namespace, @project, scope: 'are_internal') do - Internal - %span.badge - = @project.snippets.are_internal.count - - %li{ class: ("active" if params[:scope] == "are_public") } - = link_to namespace_project_snippets_path(@project.namespace, @project, scope: 'are_public') do - Public - %span.badge - = @project.snippets.are_public.count + - include_private = @project.team.member?(current_user) || current_user.admin? + = render partial: 'snippets/snippets_scope_menu', locals: { subject: @project, include_private: include_private } .nav-controls.hidden-xs - if can?(current_user, :create_project_snippet, @project) diff --git a/app/views/snippets/_snippets_scope_menu.html.haml b/app/views/snippets/_snippets_scope_menu.html.haml new file mode 100644 index 00000000000000..cb837f1fac1849 --- /dev/null +++ b/app/views/snippets/_snippets_scope_menu.html.haml @@ -0,0 +1,31 @@ +- subject = local_assigns.fetch(:subject, current_user) +- include_private = local_assigns.fetch(:include_private, false) + +.nav-links.snippet-scope-menu + %li{ class: ("active" unless params[:scope]) } + = link_to snippets_path(subject) do + All + %span.badge + - if include_private + = subject.snippets.count + - else + = subject.snippets.public_and_internal.count + + - if include_private + %li{ class: ("active" if params[:scope] == "are_private") } + = link_to snippets_path(subject, scope: 'are_private') do + Private + %span.badge + = subject.snippets.are_private.count + + %li{ class: ("active" if params[:scope] == "are_internal") } + = link_to snippets_path(subject, scope: 'are_internal') do + Internal + %span.badge + = subject.snippets.are_internal.count + + %li{ class: ("active" if params[:scope] == "are_public") } + = link_to snippets_path(subject, scope: 'are_public') do + Public + %span.badge + = subject.snippets.are_public.count -- GitLab From a68735d4985bf5ffaeaf5a051b40f8aed0c0a6e0 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 9 Dec 2016 15:43:02 -0600 Subject: [PATCH 022/191] use Snippet.none in favor of nil to allow chaining --- app/finders/snippets_finder.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb index 31f039b5a70e97..78a2f8840ed2cb 100644 --- a/app/finders/snippets_finder.rb +++ b/app/finders/snippets_finder.rb @@ -51,7 +51,7 @@ def by_project(current_user, project, scope) def by_scope(snippets, scope = nil, include_private = false) case scope.to_s when 'are_private' - include_private ? snippets.are_private : nil + include_private ? snippets.are_private : Snippet.none when 'are_internal' snippets.are_internal when 'are_public' -- GitLab From dccd53e1ce1903f2df8dd20023e2bafd96d850f3 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 9 Dec 2016 16:01:08 -0600 Subject: [PATCH 023/191] add new tests for snippets_finder.rb --- spec/finders/snippets_finder_spec.rb | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb index 28bdc18e8405e2..21ccdaa6c380b3 100644 --- a/spec/finders/snippets_finder_spec.rb +++ b/spec/finders/snippets_finder_spec.rb @@ -84,16 +84,39 @@ expect(snippets).not_to include(@snippet1, @snippet2) end - it "returns public and internal snippets for none project members" do + it "returns public and internal snippets for non project members" do snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1) expect(snippets).to include(@snippet2, @snippet3) expect(snippets).not_to include(@snippet1) end + it "returns public snippets for non project members" do + snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_public") + expect(snippets).to include(@snippet3) + expect(snippets).not_to include(@snippet1, @snippet2) + end + + it "returns internal snippets for non project members" do + snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_internal") + expect(snippets).to include(@snippet2) + expect(snippets).not_to include(@snippet1, @snippet3) + end + + it "does not return private snippets for non project members" do + snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_private") + expect(snippets).not_to include(@snippet1, @snippet2, @snippet3) + end + it "returns all snippets for project members" do project1.team << [user, :developer] snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1) expect(snippets).to include(@snippet1, @snippet2, @snippet3) end + + it "returns private snippets for project members" do + project1.team << [user, :developer] + snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1, scope: "are_private") + expect(snippets).to include(@snippet1) + end end end -- GitLab From 730ff2e50b600f3e3c79e69fd4978faf6a06ce1b Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 9 Dec 2016 17:33:23 -0600 Subject: [PATCH 024/191] rename snippets_path helper due to conflict --- app/helpers/snippets_helper.rb | 2 +- app/views/snippets/_snippets_scope_menu.html.haml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb index fc7febd338561b..8c02b4061ca047 100644 --- a/app/helpers/snippets_helper.rb +++ b/app/helpers/snippets_helper.rb @@ -11,7 +11,7 @@ def reliable_snippet_path(snippet, opts = nil) # Return the path of a snippets index for a user or for a project # # @returns String, path to snippet index - def snippets_path(subject = nil, opts = nil) + def subject_snippets_path(subject = nil, opts = nil) if subject.is_a?(Project) namespace_project_snippets_path(subject.namespace, subject, opts) else # assume subject === User diff --git a/app/views/snippets/_snippets_scope_menu.html.haml b/app/views/snippets/_snippets_scope_menu.html.haml index cb837f1fac1849..2dda5fed647942 100644 --- a/app/views/snippets/_snippets_scope_menu.html.haml +++ b/app/views/snippets/_snippets_scope_menu.html.haml @@ -3,7 +3,7 @@ .nav-links.snippet-scope-menu %li{ class: ("active" unless params[:scope]) } - = link_to snippets_path(subject) do + = link_to subject_snippets_path(subject) do All %span.badge - if include_private @@ -13,19 +13,19 @@ - if include_private %li{ class: ("active" if params[:scope] == "are_private") } - = link_to snippets_path(subject, scope: 'are_private') do + = link_to subject_snippets_path(subject, scope: 'are_private') do Private %span.badge = subject.snippets.are_private.count %li{ class: ("active" if params[:scope] == "are_internal") } - = link_to snippets_path(subject, scope: 'are_internal') do + = link_to subject_snippets_path(subject, scope: 'are_internal') do Internal %span.badge = subject.snippets.are_internal.count %li{ class: ("active" if params[:scope] == "are_public") } - = link_to snippets_path(subject, scope: 'are_public') do + = link_to subject_snippets_path(subject, scope: 'are_public') do Public %span.badge = subject.snippets.are_public.count -- GitLab From e117cb3a1965c15d3d5617addb695f337dcf069f Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Sun, 11 Dec 2016 23:22:18 -0700 Subject: [PATCH 025/191] Update Sidekiq from 4.2.1 to 4.2.7. Includes various bug fixes, mostly for Rails 5. Changelog: https://github.com/mperham/sidekiq/blob/fc168fe393bee3ad1fcbb52cff2d84af86c38cc4/Changes.md --- Gemfile | 2 +- Gemfile.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index f27d6363e3d5a6..2cc7764e6b8f04 100644 --- a/Gemfile +++ b/Gemfile @@ -132,7 +132,7 @@ gem 'after_commit_queue', '~> 1.3.0' gem 'acts-as-taggable-on', '~> 4.0' # Background jobs -gem 'sidekiq', '~> 4.2' +gem 'sidekiq', '~> 4.2.7' gem 'sidekiq-cron', '~> 0.4.4' gem 'redis-namespace', '~> 1.5.2' gem 'sidekiq-limit_fetch', '~> 3.4' diff --git a/Gemfile.lock b/Gemfile.lock index c464ff70587ad8..3de1a7cbf26217 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -126,7 +126,7 @@ GEM coffee-script-source (1.10.0) colorize (0.7.7) concurrent-ruby (1.0.2) - connection_pool (2.2.0) + connection_pool (2.2.1) crack (0.4.3) safe_yaml (~> 1.0.0) creole (0.5.0) @@ -648,10 +648,10 @@ GEM rack shoulda-matchers (2.8.0) activesupport (>= 3.0.0) - sidekiq (4.2.1) + sidekiq (4.2.7) concurrent-ruby (~> 1.0) connection_pool (~> 2.2, >= 2.2.0) - rack-protection (~> 1.5) + rack-protection (>= 1.5.0) redis (~> 3.2, >= 3.2.1) sidekiq-cron (0.4.4) redis-namespace (>= 1.5.2) @@ -928,7 +928,7 @@ DEPENDENCIES settingslogic (~> 2.0.9) sham_rack (~> 1.3.6) shoulda-matchers (~> 2.8.0) - sidekiq (~> 4.2) + sidekiq (~> 4.2.7) sidekiq-cron (~> 0.4.4) sidekiq-limit_fetch (~> 3.4) simplecov (= 0.12.0) -- GitLab From 7c9a85e35386623bc26374e4eb9b4a53e4fbbce7 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 9 Dec 2016 14:37:41 +0000 Subject: [PATCH 026/191] Fix TypeError: Cannot read property 'initTabs' --- app/assets/javascripts/pipelines.js.es6 | 2 +- changelogs/unreleased/25483-broken-tabs.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/25483-broken-tabs.yml diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6 index 72c6c4a1fcd393..a7a384fd856ef9 100644 --- a/app/assets/javascripts/pipelines.js.es6 +++ b/app/assets/javascripts/pipelines.js.es6 @@ -4,7 +4,7 @@ ((global) => { class Pipelines { - constructor(options) { + constructor(options = {}) { if (options.initTabs && options.tabsOptions) { new global.LinkedTabs(options.tabsOptions); diff --git a/changelogs/unreleased/25483-broken-tabs.yml b/changelogs/unreleased/25483-broken-tabs.yml new file mode 100644 index 00000000000000..7bc50bdf860adc --- /dev/null +++ b/changelogs/unreleased/25483-broken-tabs.yml @@ -0,0 +1,4 @@ +--- +title: Fix TypeError: Cannot read property 'initTabs' on commit builds tab +merge_request: +author: -- GitLab From 401a2ec0b159b3c5f4de617768b9a0489a7cdde3 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 9 Dec 2016 15:13:20 +0000 Subject: [PATCH 027/191] Adds tests to prevent future errors. Fix undefined variable in es5 --- app/assets/javascripts/pipelines.js.es6 | 2 +- changelogs/unreleased/25483-broken-tabs.yml | 2 +- .../fixtures/pipeline_graph.html.haml | 15 +++++++++++ spec/javascripts/pipelines_spec.js.es6 | 25 +++++++++++++++++++ 4 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 spec/javascripts/fixtures/pipeline_graph.html.haml create mode 100644 spec/javascripts/pipelines_spec.js.es6 diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6 index a7a384fd856ef9..fd1e320dc3537c 100644 --- a/app/assets/javascripts/pipelines.js.es6 +++ b/app/assets/javascripts/pipelines.js.es6 @@ -16,7 +16,7 @@ addMarginToBuildColumns() { this.pipelineGraph = document.querySelector('.pipeline-graph'); const secondChildBuildNodes = document.querySelector('.pipeline-graph').querySelectorAll('.build:nth-child(2)'); - for (buildNodeIndex in secondChildBuildNodes) { + for (const buildNodeIndex in secondChildBuildNodes) { const buildNode = secondChildBuildNodes[buildNodeIndex]; const firstChildBuildNode = buildNode.previousElementSibling; if (!firstChildBuildNode || !firstChildBuildNode.matches('.build')) continue; diff --git a/changelogs/unreleased/25483-broken-tabs.yml b/changelogs/unreleased/25483-broken-tabs.yml index 7bc50bdf860adc..d6c92014bea37c 100644 --- a/changelogs/unreleased/25483-broken-tabs.yml +++ b/changelogs/unreleased/25483-broken-tabs.yml @@ -1,4 +1,4 @@ --- title: Fix TypeError: Cannot read property 'initTabs' on commit builds tab -merge_request: +merge_request: 8009 author: diff --git a/spec/javascripts/fixtures/pipeline_graph.html.haml b/spec/javascripts/fixtures/pipeline_graph.html.haml new file mode 100644 index 00000000000000..be5f105767cdd0 --- /dev/null +++ b/spec/javascripts/fixtures/pipeline_graph.html.haml @@ -0,0 +1,15 @@ +%div.pipeline-visualization.pipeline-graph + %ul.stage-column-list + %li.stage-column + .stage-name + %a{:href => "/"} + Test + .builds-container + %ul + %li.build + .curve + .build-content + %a + %svg + .ci-status-text + stop_review diff --git a/spec/javascripts/pipelines_spec.js.es6 b/spec/javascripts/pipelines_spec.js.es6 new file mode 100644 index 00000000000000..85c9cf4b4f1d0f --- /dev/null +++ b/spec/javascripts/pipelines_spec.js.es6 @@ -0,0 +1,25 @@ +//= require pipelines + +(() => { + describe('Pipelines', () => { + fixture.preload('pipeline_graph'); + + beforeEach(() => { + fixture.load('pipeline_graph'); + }); + + it('should be defined', () => { + expect(window.gl.Pipelines).toBeDefined(); + }); + + it('should create a `Pipelines` instance without options', () => { + expect(() => { new window.gl.Pipelines(); }).not.toThrow(); //eslint-disable-line + }); + + it('should create a `Pipelines` instance with options', () => { + const pipelines = new window.gl.Pipelines({ foo: 'bar' }); + + expect(pipelines.pipelineGraph).toBeDefined(); + }); + }); +})(); -- GitLab From 94e0f402334af845bb44e92a3e2646780d633ce2 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 9 Dec 2016 16:14:54 +0000 Subject: [PATCH 028/191] Fix Pipeline graph disappeared from the builds tab in commits and merge request views --- app/assets/javascripts/pipelines.js.es6 | 4 ++-- app/views/projects/commit/_pipeline.html.haml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6 index fd1e320dc3537c..f09c6bb7def889 100644 --- a/app/assets/javascripts/pipelines.js.es6 +++ b/app/assets/javascripts/pipelines.js.es6 @@ -14,8 +14,8 @@ } addMarginToBuildColumns() { - this.pipelineGraph = document.querySelector('.pipeline-graph'); - const secondChildBuildNodes = document.querySelector('.pipeline-graph').querySelectorAll('.build:nth-child(2)'); + this.pipelineGraph = document.querySelector('.js-pipeline-graph'); + const secondChildBuildNodes = document.querySelector('.js-pipeline-graph').querySelectorAll('.build:nth-child(2)'); for (const buildNodeIndex in secondChildBuildNodes) { const buildNode = secondChildBuildNodes[buildNodeIndex]; const firstChildBuildNode = buildNode.previousElementSibling; diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml index c7b5c1124b37a5..08d3443b3d02f8 100644 --- a/app/views/projects/commit/_pipeline.html.haml +++ b/app/views/projects/commit/_pipeline.html.haml @@ -24,7 +24,7 @@ in = time_interval_in_words pipeline.duration - .row-content-block.build-content.middle-block.hidden + .row-content-block.build-content.middle-block.js-pipeline-graph.hidden = render "projects/pipelines/graph", pipeline: pipeline - if pipeline.yaml_errors.present? -- GitLab From 52e0c4ba916d2cbc9bdb0fa0782c6b705c03c5a6 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 9 Dec 2016 16:16:14 +0000 Subject: [PATCH 029/191] Fix tests Fix broken tests --- app/assets/javascripts/pipelines.js.es6 | 5 ++++- app/views/projects/ci/builds/_build_pipeline.html.haml | 4 ++-- .../_generic_commit_status_pipeline.html.haml | 2 +- app/views/projects/pipelines/_with_tabs.html.haml | 2 +- spec/javascripts/fixtures/pipeline_graph.html.haml | 2 +- spec/views/projects/pipelines/show.html.haml_spec.rb | 2 +- 6 files changed, 10 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6 index f09c6bb7def889..fb95648e1c74cf 100644 --- a/app/assets/javascripts/pipelines.js.es6 +++ b/app/assets/javascripts/pipelines.js.es6 @@ -15,7 +15,9 @@ addMarginToBuildColumns() { this.pipelineGraph = document.querySelector('.js-pipeline-graph'); - const secondChildBuildNodes = document.querySelector('.js-pipeline-graph').querySelectorAll('.build:nth-child(2)'); + + const secondChildBuildNodes = this.pipelineGraph.querySelectorAll('.build:nth-child(2)'); + for (const buildNodeIndex in secondChildBuildNodes) { const buildNode = secondChildBuildNodes[buildNodeIndex]; const firstChildBuildNode = buildNode.previousElementSibling; @@ -28,6 +30,7 @@ const columnBuilds = previousColumn.querySelectorAll('.build'); if (columnBuilds.length === 1) previousColumn.classList.add('no-margin'); } + this.pipelineGraph.classList.remove('hidden'); } } diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml index 423a1282eb2f61..ad1a7360a8b3e8 100644 --- a/app/views/projects/ci/builds/_build_pipeline.html.haml +++ b/app/views/projects/ci/builds/_build_pipeline.html.haml @@ -1,10 +1,10 @@ - is_playable = subject.playable? && can?(current_user, :update_build, @project) - if is_playable - = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, data: { toggle: 'tooltip', title: "#{subject.name} - play", container: '.pipeline-graph', placement: 'bottom' } do + = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, data: { toggle: 'tooltip', title: "#{subject.name} - play", container: '.js-pipeline-graph', placement: 'bottom' } do = ci_icon_for_status('play') .ci-status-text= subject.name - elsif can?(current_user, :read_build, @project) - = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject), data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } do + = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject), data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.js-pipeline-graph', placement: 'bottom' } do %span{class: "ci-status-icon ci-status-icon-#{subject.status}"} = ci_icon_for_status(subject.status) .ci-status-text= subject.name diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml index 7b82d913d293a3..1bba04431542e1 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml @@ -1,4 +1,4 @@ -%a{ data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } } +%a{ data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.js-pipeline-graph', placement: 'bottom' } } - if subject.target_url = link_to subject.target_url do %span{class: "ci-status-icon ci-status-icon-#{subject.status}"} diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml index 739e59308224cb..88af41aa835169 100644 --- a/app/views/projects/pipelines/_with_tabs.html.haml +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -12,7 +12,7 @@ .tab-content #js-tab-pipeline.tab-pane - .build-content.middle-block + .build-content.middle-block.js-pipeline-graph = render "projects/pipelines/graph", pipeline: pipeline #js-tab-builds.tab-pane diff --git a/spec/javascripts/fixtures/pipeline_graph.html.haml b/spec/javascripts/fixtures/pipeline_graph.html.haml index be5f105767cdd0..deca50ceaa74b5 100644 --- a/spec/javascripts/fixtures/pipeline_graph.html.haml +++ b/spec/javascripts/fixtures/pipeline_graph.html.haml @@ -1,4 +1,4 @@ -%div.pipeline-visualization.pipeline-graph +%div.pipeline-visualization.js-pipeline-graph %ul.stage-column-list %li.stage-column .stage-name diff --git a/spec/views/projects/pipelines/show.html.haml_spec.rb b/spec/views/projects/pipelines/show.html.haml_spec.rb index bf027499c94c59..a066ea078e65bd 100644 --- a/spec/views/projects/pipelines/show.html.haml_spec.rb +++ b/spec/views/projects/pipelines/show.html.haml_spec.rb @@ -28,7 +28,7 @@ it 'shows a graph with grouped stages' do render - expect(rendered).to have_css('.pipeline-graph') + expect(rendered).to have_css('.js-pipeline-graph') expect(rendered).to have_css('.grouped-pipeline-dropdown') # stages -- GitLab From 62f8717c035f8d287324d27563b3a42fd27839d6 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Fri, 25 Nov 2016 18:42:25 +0000 Subject: [PATCH 030/191] Added hiddenInterval and immediateExecution settings, fixed visibilitychange listening, implemented with mr widget Updated tests Added tests Review changes --- .../javascripts/merge_request_widget.js.es6 | 74 +++++++------------ app/assets/javascripts/smart_interval.js.es6 | 69 +++++++++++------ .../24807-stop-ddosing-ourselves.yml | 4 + spec/javascripts/smart_interval_spec.js.es6 | 35 +++++++-- 4 files changed, 105 insertions(+), 77 deletions(-) create mode 100644 changelogs/unreleased/24807-stop-ddosing-ourselves.yml diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6 index d9495e503888ac..7022aa1263b8ee 100644 --- a/app/assets/javascripts/merge_request_widget.js.es6 +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -40,19 +40,26 @@ $('#modal_merge_info').modal({ show: false }); - this.firstCICheck = true; - this.readyForCICheck = false; - this.readyForCIEnvironmentCheck = false; - this.cancel = false; - clearInterval(this.fetchBuildStatusInterval); - clearInterval(this.fetchBuildEnvironmentStatusInterval); this.clearEventListeners(); this.addEventListeners(); this.getCIStatus(false); - this.getCIEnvironmentsStatus(); this.retrieveSuccessIcon(); - this.pollCIStatus(); - this.pollCIEnvironmentsStatus(); + + this.ciStatusInterval = new global.SmartInterval({ + callback: this.getCIStatus.bind(this, true), + startingInterval: 10000, + maxInterval: 30000, + hiddenInterval: 120000, + incrementByFactorOf: 5000, + }); + this.ciEnvironmentStatusInterval = new global.SmartInterval({ + callback: this.getCIEnvironmentsStatus.bind(this), + startingInterval: 30000, + maxInterval: 120000, + hiddenInterval: 240000, + incrementByFactorOf: 15000, + immediateExecution: true, + }); notifyPermissions(); } @@ -60,10 +67,6 @@ return $(document).off('page:change.merge_request'); }; - MergeRequestWidget.prototype.cancelPolling = function() { - return this.cancel = true; - }; - MergeRequestWidget.prototype.addEventListeners = function() { var allowedPages; allowedPages = ['show', 'commits', 'builds', 'pipelines', 'changes']; @@ -72,9 +75,6 @@ var page; page = $('body').data('page').split(':').last(); if (allowedPages.indexOf(page) < 0) { - clearInterval(_this.fetchBuildStatusInterval); - clearInterval(_this.fetchBuildEnvironmentStatusInterval); - _this.cancelPolling(); return _this.clearEventListeners(); } }; @@ -114,6 +114,11 @@ }); }; + MergeRequestWidget.prototype.cancelPolling = function () { + this.ciStatusInterval.cancel(); + this.ciEnvironmentStatusInterval.cancel(); + }; + MergeRequestWidget.prototype.getMergeStatus = function() { return $.get(this.opts.merge_check_url, function(data) { return $('.mr-state-widget').replaceWith(data); @@ -131,18 +136,6 @@ } }; - MergeRequestWidget.prototype.pollCIStatus = function() { - return this.fetchBuildStatusInterval = setInterval(((function(_this) { - return function() { - if (!_this.readyForCICheck) { - return; - } - _this.getCIStatus(true); - return _this.readyForCICheck = false; - }; - })(this)), 10000); - }; - MergeRequestWidget.prototype.getCIStatus = function(showNotification) { var _this; _this = this; @@ -150,23 +143,17 @@ return $.getJSON(this.opts.ci_status_url, (function(_this) { return function(data) { var message, status, title; - if (_this.cancel) { - return; - } - _this.readyForCICheck = true; if (data.status === '') { return; } if (data.environments && data.environments.length) _this.renderEnvironments(data.environments); - if (_this.firstCICheck || data.status !== _this.opts.ci_status && (data.status != null)) { + if (data.status !== _this.opts.ci_status && (data.status != null)) { _this.opts.ci_status = data.status; _this.showCIStatus(data.status); if (data.coverage) { _this.showCICoverage(data.coverage); } - // The first check should only update the UI, a notification - // should only be displayed on status changes - if (showNotification && !_this.firstCICheck) { + if (showNotification) { status = _this.ciLabelForStatus(data.status); if (status === "preparing") { title = _this.opts.ci_title.preparing; @@ -184,24 +171,13 @@ return Turbolinks.visit(_this.opts.builds_path); }); } - return _this.firstCICheck = false; } }; })(this)); }; - MergeRequestWidget.prototype.pollCIEnvironmentsStatus = function() { - this.fetchBuildEnvironmentStatusInterval = setInterval(() => { - if (!this.readyForCIEnvironmentCheck) return; - this.getCIEnvironmentsStatus(); - this.readyForCIEnvironmentCheck = false; - }, 300000); - }; - MergeRequestWidget.prototype.getCIEnvironmentsStatus = function() { $.getJSON(this.opts.ci_environments_status_url, (environments) => { - if (this.cancel) return; - this.readyForCIEnvironmentCheck = true; if (environments && environments.length) this.renderEnvironments(environments); }); }; @@ -212,11 +188,11 @@ if ($(`.mr-state-widget #${ environment.id }`).length) return; const $template = $(DEPLOYMENT_TEMPLATE); if (!environment.external_url || !environment.external_url_formatted) $('.js-environment-link', $template).remove(); - + if (!environment.stop_url) { $('.js-stop-env-link', $template).remove(); } - + if (environment.deployed_at && environment.deployed_at_formatted) { environment.deployed_at = gl.utils.getTimeago().format(environment.deployed_at, 'gl_en') + '.'; } else { diff --git a/app/assets/javascripts/smart_interval.js.es6 b/app/assets/javascripts/smart_interval.js.es6 index 5eb15dba79b78e..40f67637c7c8f6 100644 --- a/app/assets/javascripts/smart_interval.js.es6 +++ b/app/assets/javascripts/smart_interval.js.es6 @@ -7,24 +7,31 @@ (() => { class SmartInterval { /** - * @param { function } callback Function to be called on each iteration (required) - * @param { milliseconds } startingInterval `currentInterval` is set to this initially - * @param { milliseconds } maxInterval `currentInterval` will be incremented to this - * @param { integer } incrementByFactorOf `currentInterval` is incremented by this factor - * @param { boolean } lazyStart Configure if timer is initialized on instantiation or lazily + * @param { function } opts.callback Function to be called on each iteration (required) + * @param { milliseconds } opts.startingInterval `currentInterval` is set to this initially + * @param { milliseconds } opts.maxInterval `currentInterval` will be incremented to this + * @param { milliseconds } opts.hiddenInterval `currentInterval` is set to this + * when the page is hidden + * @param { integer } opts.incrementByFactorOf `currentInterval` is incremented by this factor + * @param { boolean } opts.lazyStart Configure if timer is initialized on + * instantiation or lazily + * @param { boolean } opts.immediateExecution Configure if callback should + * be executed before the first interval. */ - constructor({ callback, startingInterval, maxInterval, incrementByFactorOf, lazyStart }) { + constructor(opts = {}) { this.cfg = { - callback, - startingInterval, - maxInterval, - incrementByFactorOf, - lazyStart, + callback: opts.callback, + startingInterval: opts.startingInterval, + maxInterval: opts.maxInterval, + hiddenInterval: opts.hiddenInterval, + incrementByFactorOf: opts.incrementByFactorOf, + lazyStart: opts.lazyStart, + immediateExecution: opts.immediateExecution, }; this.state = { intervalId: null, - currentInterval: startingInterval, + currentInterval: this.cfg.startingInterval, pageVisibility: 'visible', }; @@ -36,6 +43,11 @@ const cfg = this.cfg; const state = this.state; + if (cfg.immediateExecution) { + cfg.immediateExecution = false; + cfg.callback(); + } + state.intervalId = window.setInterval(() => { cfg.callback(); @@ -54,14 +66,29 @@ this.stopTimer(); } + onVisibilityHidden() { + if (this.cfg.hiddenInterval) { + this.setCurrentInterval(this.cfg.hiddenInterval); + this.resume(); + } else { + this.cancel(); + } + } + // start a timer, using the existing interval resume() { this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped this.start(); } + onVisibilityVisible() { + this.cancel(); + this.start(); + } + destroy() { this.cancel(); + document.removeEventListener('visibilitychange', this.handleVisibilityChange); $(document).off('visibilitychange').off('page:before-unload'); } @@ -80,11 +107,7 @@ initVisibilityChangeHandling() { // cancel interval when tab no longer shown (prevents cached pages from polling) - $(document) - .off('visibilitychange').on('visibilitychange', (e) => { - this.state.pageVisibility = e.target.visibilityState; - this.handleVisibilityChange(); - }); + document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this)); } initPageUnloadHandling() { @@ -92,10 +115,11 @@ $(document).on('page:before-unload', () => this.cancel()); } - handleVisibilityChange() { - const state = this.state; - - const intervalAction = state.pageVisibility === 'hidden' ? this.cancel : this.resume; + handleVisibilityChange(e) { + this.state.pageVisibility = e.target.visibilityState; + const intervalAction = this.isPageVisible() ? + this.onVisibilityVisible : + this.onVisibilityHidden; intervalAction.apply(this); } @@ -111,6 +135,7 @@ incrementInterval() { const cfg = this.cfg; const currentInterval = this.getCurrentInterval(); + if (cfg.hiddenInterval && !this.isPageVisible()) return; let nextInterval = currentInterval * cfg.incrementByFactorOf; if (nextInterval > cfg.maxInterval) { @@ -120,6 +145,8 @@ this.setCurrentInterval(nextInterval); } + isPageVisible() { return this.state.pageVisibility === 'visible'; } + stopTimer() { const state = this.state; diff --git a/changelogs/unreleased/24807-stop-ddosing-ourselves.yml b/changelogs/unreleased/24807-stop-ddosing-ourselves.yml new file mode 100644 index 00000000000000..49e6c5e56e5987 --- /dev/null +++ b/changelogs/unreleased/24807-stop-ddosing-ourselves.yml @@ -0,0 +1,4 @@ +--- +title: Use SmartInterval for MR widget and improve visibilitychange functionality +merge_request: 7762 +author: diff --git a/spec/javascripts/smart_interval_spec.js.es6 b/spec/javascripts/smart_interval_spec.js.es6 index ed6166a25a83b4..1b7ca97cde48c6 100644 --- a/spec/javascripts/smart_interval_spec.js.es6 +++ b/spec/javascripts/smart_interval_spec.js.es6 @@ -14,8 +14,9 @@ startingInterval: DEFAULT_STARTING_INTERVAL, maxInterval: DEFAULT_MAX_INTERVAL, incrementByFactorOf: DEFAULT_INCREMENT_FACTOR, - delayStartBy: 0, lazyStart: false, + immediateExecution: false, + hiddenInterval: null, }; if (config) { @@ -114,14 +115,31 @@ expect(interval.state.intervalId).toBeTruthy(); // simulates triggering of visibilitychange event - interval.state.pageVisibility = 'hidden'; - interval.handleVisibilityChange(); + interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } }); expect(interval.state.intervalId).toBeUndefined(); done(); }, DEFAULT_SHORT_TIMEOUT); }); + it('should change to the hidden interval when page is not visible', function (done) { + const HIDDEN_INTERVAL = 1500; + const interval = createDefaultSmartInterval({ hiddenInterval: HIDDEN_INTERVAL }); + + setTimeout(() => { + expect(interval.state.intervalId).toBeTruthy(); + expect(interval.getCurrentInterval() >= DEFAULT_STARTING_INTERVAL && + interval.getCurrentInterval() <= DEFAULT_MAX_INTERVAL).toBeTruthy(); + + // simulates triggering of visibilitychange event + interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } }); + + expect(interval.state.intervalId).toBeTruthy(); + expect(interval.getCurrentInterval()).toBe(HIDDEN_INTERVAL); + done(); + }, DEFAULT_SHORT_TIMEOUT); + }); + it('should resume when page is becomes visible at the previous interval', function (done) { const interval = this.smartInterval; @@ -129,14 +147,12 @@ expect(interval.state.intervalId).toBeTruthy(); // simulates triggering of visibilitychange event - interval.state.pageVisibility = 'hidden'; - interval.handleVisibilityChange(); + interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } }); expect(interval.state.intervalId).toBeUndefined(); // simulates triggering of visibilitychange event - interval.state.pageVisibility = 'visible'; - interval.handleVisibilityChange(); + interval.handleVisibilityChange({ target: { visibilityState: 'visible' } }); expect(interval.state.intervalId).toBeTruthy(); @@ -154,6 +170,11 @@ done(); }, DEFAULT_SHORT_TIMEOUT); }); + + it('should execute callback before first interval', function () { + const interval = createDefaultSmartInterval({ immediateExecution: true }); + expect(interval.cfg.immediateExecution).toBeFalsy(); + }); }); }); })(window.gl || (window.gl = {})); -- GitLab From 8d0645c2ce3769268020ad6f8e51db07fb1e4bc6 Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Mon, 12 Dec 2016 14:27:52 +0100 Subject: [PATCH 031/191] Grapify the service API --- doc/api/services.md | 22 ++++++++++++++----- .../project_services/hipchat_service_spec.rb | 2 +- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/doc/api/services.md b/doc/api/services.md index acb54448664b00..3dad953cd1e0e6 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -153,9 +153,12 @@ PUT /projects/:id/services/builds-email Parameters: -- `recipients` (**required**) - Comma-separated list of recipient email addresses -- `add_pusher` (optional) - Add pusher to recipients list -- `notify_only_broken_builds` (optional) -Notify only broken builds +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `recipients` | string | yes | Comma-separated list of recipient email addresses | +| `add_pusher` | boolean | no | Add pusher to recipients list | +| `notify_only_broken_builds` | boolean | no | Notify only broken builds | + ### Delete Build-Emails service @@ -538,7 +541,10 @@ PUT /projects/:id/services/mattermost-slash-commands Parameters: -- `token` (**required**) - The Mattermost token +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `token` | string | yes | The Mattermost token | + ### Delete Mattermost Slash Command service @@ -570,8 +576,12 @@ PUT /projects/:id/services/pipelines-email Parameters: -- `recipients` (**required**) - Comma-separated list of recipient email addresses -- `notify_only_broken_builds` (optional) -Notify only broken pipelines +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `recipients` | string | yes | Comma-separated list of recipient email addresses | +| `add_pusher` | boolean | no | Add pusher to recipients list | +| `notify_only_broken_builds` | boolean | no | Notify only broken pipelines | + ### Delete Pipeline-Emails service diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index 564e49d5459ed6..2da3a9cb09f446 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -358,7 +358,7 @@ context 'with a failed build' do it 'uses the red color' do build_data = { object_kind: 'build', commit: { status: 'failed' } } - + expect(hipchat.__send__(:message_options, build_data)).to eq({ notify: false, color: 'red' }) end end -- GitLab From 17e3d3fde8c9a83f58d797c5f62f36b59eedd870 Mon Sep 17 00:00:00 2001 From: winniehell Date: Tue, 6 Dec 2016 01:29:43 +0100 Subject: [PATCH 032/191] Avoid escaping relative links in Markdown twice (!7940) --- changelogs/unreleased/unescape-relative-path.yml | 4 ++++ lib/banzai/filter/relative_link_filter.rb | 14 ++++++-------- 2 files changed, 10 insertions(+), 8 deletions(-) create mode 100644 changelogs/unreleased/unescape-relative-path.yml diff --git a/changelogs/unreleased/unescape-relative-path.yml b/changelogs/unreleased/unescape-relative-path.yml new file mode 100644 index 00000000000000..755b0379a16bd2 --- /dev/null +++ b/changelogs/unreleased/unescape-relative-path.yml @@ -0,0 +1,4 @@ +--- +title: Avoid escaping relative links in Markdown twice +merge_request: 7940 +author: winniehell diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb index f09d78be0cee19..9e23c8f8c553d2 100644 --- a/lib/banzai/filter/relative_link_filter.rb +++ b/lib/banzai/filter/relative_link_filter.rb @@ -46,7 +46,7 @@ def process_link_attr(html_attr) end def rebuild_relative_uri(uri) - file_path = relative_file_path(uri.path) + file_path = relative_file_path(uri) uri.path = [ relative_url_root, @@ -59,8 +59,10 @@ def rebuild_relative_uri(uri) uri end - def relative_file_path(path) - nested_path = build_relative_path(path, context[:requested_path]) + def relative_file_path(uri) + path = Addressable::URI.unescape(uri.path) + request_path = Addressable::URI.unescape(context[:requested_path]) + nested_path = build_relative_path(path, request_path) file_exists?(nested_path) ? nested_path : path end @@ -108,11 +110,7 @@ def file_exists?(path) end def uri_type(path) - @uri_types[path] ||= begin - unescaped_path = Addressable::URI.unescape(path) - - current_commit.uri_type(unescaped_path) - end + @uri_types[path] ||= current_commit.uri_type(path) end def current_commit -- GitLab From b67df6d071d54fff1befd435c4185ecab3e20f4f Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 4 Nov 2016 16:27:11 -0500 Subject: [PATCH 033/191] Add basic search --- app/assets/javascripts/dispatcher.js.es6 | 3 + .../filtered_search/filtered_search_bundle.js | 13 +++ .../filtered_search_manager.js.es6 | 104 ++++++++++++++++++ app/assets/stylesheets/framework/filters.scss | 24 ++++ app/views/projects/issues/index.html.haml | 6 +- .../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 413117c2226831..65ae17c93a75eb 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -35,6 +35,9 @@ break; case 'projects:merge_requests:index': case 'projects:issues:index': + if(gl.hasOwnProperty('FilteredSearchManager')) { + new gl.FilteredSearchManager(); + } Issuable.init(); new gl.IssuableBulkActions(); shortcut_handler = new ShortcutsNavigation(); 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 00000000000000..656979ba82f951 --- /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 00000000000000..797473f2044c89 --- /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 19827943385a10..a565642ba38208 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 26f3f0ac292c10..18e8372ecab670 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 00000000000000..40c1bd3ef989b8 --- /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 0aa2873f94a4be..de7c133bc65033 100644 --- a/config/application.rb +++ b/config/application.rb @@ -98,6 +98,7 @@ class Application < Rails::Application config.assets.precompile << "environments/environments_bundle.js" config.assets.precompile << "blob_edit/blob_edit_bundle.js" config.assets.precompile << "snippet/snippet_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" -- GitLab From 32442e6c13c40a3be6c7ff396b112056e3c2548b Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 7 Nov 2016 16:18:50 -0600 Subject: [PATCH 034/191] Add filter params to 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 797473f2044c89..c26a46a8558e48 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) { -- GitLab From 76ea6999c923fe41dd7bc29cdae7596ad5d44590 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 7 Nov 2016 16:19:15 -0600 Subject: [PATCH 035/191] Load searched params into input field --- .../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 c26a46a8558e48..44718e8306c290 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(); -- GitLab From 1f584de511913a06f73f1ab0cf88d28644620d72 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 7 Nov 2016 16:19:30 -0600 Subject: [PATCH 036/191] 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 40c1bd3ef989b8..db9011d5d57449 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(); -- GitLab From 103e5a7fa97b36fba689d22f2f141238ca45c3a9 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 7 Nov 2016 16:33:51 -0600 Subject: [PATCH 037/191] Add author_username and assignee_username --- .../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 44718e8306c290..94c0b99a1e1103 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 b4c14d05eaf989..2afde8ece65075 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -165,31 +165,43 @@ def labels 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 @@ def sort(items) 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 @@ def by_assignee(items) end def by_author(items) - if author? + if author_id? || author_username? items = items.where(author_id: author.try(:id)) end -- GitLab From d7aede77be8aba1cc9d4fe2458c160c542279eca Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 8 Nov 2016 11:35:28 -0600 Subject: [PATCH 038/191] Sanitize spaces in search term --- .../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 94c0b99a1e1103..c9d7a99ae44447 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 = {})); -- GitLab From e04410b7e9de256998a9bac5859ab87d4142f370 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 8 Nov 2016 11:36:03 -0600 Subject: [PATCH 039/191] Fixed bug where search terms with colons were not searchable --- .../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 c9d7a99ae44447..7acdabe3ef2914 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); -- GitLab From 02de8f5a575404ff81900d9945123768addfbbd4 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 8 Nov 2016 12:47:53 -0600 Subject: [PATCH 040/191] Add clear search button --- .../filtered_search_manager.js.es6 | 21 ++++++++++++++++++ app/assets/stylesheets/framework/filters.scss | 22 +++++++++++++++++-- .../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 7acdabe3ef2914..ad988fe2072e15 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 a565642ba38208..b192455f5f0dfb 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 db9011d5d57449..5e759301a044be 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 -- GitLab From 7a680450f5422a00f1d11c8a86b9a75e976aca58 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 8 Nov 2016 12:59:30 -0600 Subject: [PATCH 041/191] Use + instead of %20 --- .../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 ad988fe2072e15..fccc0de050febb 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; -- GitLab From 5309c8e3e8b0ed62c8ed865e1e481e3ed56551b7 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 8 Nov 2016 13:20:37 -0600 Subject: [PATCH 042/191] Add search based on state --- .../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 fccc0de050febb..63cdcecdf49ce7 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) => { -- GitLab From 174c297434d326931ad0feb68cfbc33fb82a8c1b Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 8 Nov 2016 14:18:46 -0600 Subject: [PATCH 043/191] Add support for quotations --- .../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 63cdcecdf49ce7..f5e53d075b0ca3 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({ -- GitLab From 4b3aaccf942564f0fb137dc70ad0872a8de4772c Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 8 Nov 2016 15:42:15 -0600 Subject: [PATCH 044/191] Add special character encoding --- .../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 f5e53d075b0ca3..393e0b8a4b2ea8 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; -- GitLab From 837e05c49c095c9ddaa4803b783d5e832a5a3735 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 8 Nov 2016 16:20:47 -0600 Subject: [PATCH 045/191] Fix bug where search terms would not work after switching to another state tab --- .../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 393e0b8a4b2ea8..3528d9da88c1cf 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 += ' '; } }); -- GitLab From 4be4b552256a7e4c077bffdf413a8d8c4aebe4c7 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 8 Nov 2016 16:30:06 -0600 Subject: [PATCH 046/191] Fix bug where spaces would conver into + for all values --- .../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 3528d9da88c1cf..9fe70bbf3a7196 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; -- GitLab From 0630291a30bbf40e760f2cf16d88230ed6e2e398 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 8 Nov 2016 17:54:19 -0600 Subject: [PATCH 047/191] Fix bug where clear search button would not toggle visible --- .../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 9fe70bbf3a7196..42fe0cace10dc2 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 = ''; -- GitLab From 664e2a25e889df5fee1ec8de1e033d9b39787269 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 9 Nov 2016 13:51:43 -0600 Subject: [PATCH 048/191] 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 b192455f5f0dfb..90b9394b207d1f 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; -- GitLab From eb0d6a2cdd655a1e580e8bfdc639049d0056fcc1 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 9 Nov 2016 14:31:58 -0600 Subject: [PATCH 049/191] Fix ESLint errors --- .../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 42fe0cace10dc2..1b58fc016089ff 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; -- GitLab From d9e46be3a857a321821b60c17a58dc29a8bfa0e8 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 9 Nov 2016 14:44:11 -0600 Subject: [PATCH 050/191] Add droplab --- app/assets/javascripts/droplab/droplab.js | 515 ++++++++++++++++++ .../javascripts/droplab/droplab_ajax.js | 45 ++ .../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 00000000000000..18ca8be7203163 --- /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 00000000000000..23e43b352d632d --- /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: [PATCH 051/191] Refactor tokenizer --- .../filtered_search_manager.js.es6 | 120 ++++-------------- .../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 1b58fc016089ff..58c64ea078d7b8 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 00000000000000..f6cc1b8860d6c4 --- /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 = {})); -- GitLab From 87532efaf6e3f2464298726d352d4682e78a45ec Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 9 Nov 2016 19:10:15 -0600 Subject: [PATCH 052/191] Fix JS for tests --- .../filtered_search/filtered_search_manager.js.es6 | 7 ++++--- .../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 58c64ea078d7b8..db414b9755d782 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 f6cc1b8860d6c4..ddb173b2d9824f 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 = '"'; -- GitLab From f128c7ed26f322305603c54e58ebb72d50885757 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 10 Nov 2016 16:49:12 -0600 Subject: [PATCH 053/191] 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 90b9394b207d1f..c679a3833e9056 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 0d19563d6284e9..7d681742045273 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -4,147 +4,236 @@ 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 @@ 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 @@ 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]) -- GitLab From 7c03458a82fa86dd51afb71ff28882f1e0801469 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 11 Nov 2016 11:56:47 -0600 Subject: [PATCH 054/191] 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 5fa94556501560..a226b7ca0cb588 100644 --- a/app/assets/javascripts/search_autocomplete.js.es6 +++ b/app/assets/javascripts/search_autocomplete.js.es6 @@ -141,8 +141,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()]; @@ -157,10 +158,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 2c21804fe7a632..ab735315515c22 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -11,6 +11,7 @@ def add_gon_variables 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 caecd027aaa4bd..9a7079848a5eb4 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -169,16 +169,16 @@ 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 -- GitLab From 565f9a3cfcadaea1ee8fd18b4c15dd34c09da24c Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 11 Nov 2016 14:51:32 -0600 Subject: [PATCH 055/191] Move spec to check on MR page instead of Issues page --- .../filter_by_labels_spec.rb | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) rename spec/features/{issues => merge_requests}/filter_by_labels_spec.rb (83%) diff --git a/spec/features/issues/filter_by_labels_spec.rb b/spec/features/merge_requests/filter_by_labels_spec.rb similarity index 83% rename from spec/features/issues/filter_by_labels_spec.rb rename to spec/features/merge_requests/filter_by_labels_spec.rb index 0253629f753fb0..eff350ed53a1da 100644 --- a/spec/features/issues/filter_by_labels_spec.rb +++ b/spec/features/merge_requests/filter_by_labels_spec.rb @@ -7,25 +7,27 @@ 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') + 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") } - issue1 = create(:issue, title: "Bugfix1", project: project) - issue1.labels << bug + before do + mr1.labels << bug - issue2 = create(:issue, title: "Bugfix2", project: project) - issue2.labels << bug - issue2.labels << enhancement + mr2.labels << bug + mr2.labels << enhancement - issue3 = create(:issue, title: "Feature1", project: project) - issue3.labels << feature + mr3.title = "Feature1" + mr3.labels << feature project.team << [user, :master] login_as(user) - visit namespace_project_issues_path(project.namespace, project) + visit namespace_project_merge_requests_path(project.namespace, project) end context 'filter by label bug' do -- GitLab From 8e89a880ded54e6a388b810c23b081cfe5fffd2c Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 11 Nov 2016 14:53:12 -0600 Subject: [PATCH 056/191] Remove spec since it already exists in MR page --- .../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 9dfa5d1de1991e..00000000000000 --- 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 -- GitLab From 1dc38f9a6b20696352e0f8c7a119f6d92cb12bcd Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 11 Nov 2016 15:10:13 -0600 Subject: [PATCH 057/191] Convert and move reset filters spec to MR --- spec/features/issues/reset_filters_spec.rb | 89 ----------------- .../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 c9a3ecf16ea1ae..00000000000000 --- 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 00000000000000..2e468f2edf3c36 --- /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 -- GitLab From 6d2a218f756096f61c42fc7a6f866b51457670a2 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 11 Nov 2016 15:42:40 -0600 Subject: [PATCH 058/191] Add filter by merge request spec based on previous filter by issues spec --- .../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 00000000000000..4642b5a530d1c2 --- /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 -- GitLab From 82c1055b491e2104807c2947783ac0821b93561f Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 14 Nov 2016 10:25:32 -0600 Subject: [PATCH 059/191] 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 7d681742045273..2f8e7adad89f1b 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -15,6 +15,7 @@ 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 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 eff350ed53a1da..4c60329865cf7c 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 @@ 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 -- GitLab From 11429e9289e1e53dee10d40bbec6284a65cd78d8 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 14 Nov 2016 10:37:55 -0600 Subject: [PATCH 060/191] Resolve MR review suggestions --- .../filtered_search/filtered_search_bundle.js | 6 ---- .../filtered_search_manager.js.es6 | 28 +++++++++---------- .../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 656979ba82f951..d188718c5f3c1d 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 db414b9755d782..26b9d334545dce 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 ddb173b2d9824f..de91081edfa8dc 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 -- GitLab From 64750ae3a07450bcb0a16a3b37291ca0729a2a15 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 14 Nov 2016 13:22:32 -0600 Subject: [PATCH 061/191] Add token symbol matching --- .../filtered_search_manager.js.es6 | 94 ++++++++++++++----- .../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 26b9d334545dce..31e570bd6b6ac8 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 de91081edfa8dc..c3e5e817c9ec17 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; -- GitLab From bbd30534dc1c24db9c4b71d748637af3433f86a4 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 14 Nov 2016 14:43:09 -0600 Subject: [PATCH 062/191] 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 2f8e7adad89f1b..c790f350b2dd0e 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -82,13 +82,19 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 -- GitLab From 54a45a1dde00d1545b7fe1e8e3c7da4e425ac3c9 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 14 Nov 2016 16:55:38 -0600 Subject: [PATCH 063/191] 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 00000000000000..1d632671fe2afd --- /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 -- GitLab From ecb20dab1c9cd8e1ad9627cb195185129fab0f8c Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 14 Nov 2016 17:45:26 -0600 Subject: [PATCH 064/191] Fix eslint --- .../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 31e570bd6b6ac8..8568bf78416412 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}`; -- GitLab From a5826982ca24ed0798cbd1ce8444265a46312371 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 14 Nov 2016 18:19:40 -0600 Subject: [PATCH 065/191] Add more specs --- spec/features/issues/filter_issues_spec.rb | 106 ++++++++++++++---- .../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 c790f350b2dd0e..0eed0ed4274962 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -17,6 +17,8 @@ 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 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 2e468f2edf3c36..3a7ece7e1d6de1 100644 --- a/spec/features/merge_requests/reset_filters_spec.rb +++ b/spec/features/merge_requests/reset_filters_spec.rb @@ -9,7 +9,7 @@ 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' } -- GitLab From 50ee0948f0ec4da40dd6d8798c526b611a9b6411 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 14 Nov 2016 22:14:29 -0600 Subject: [PATCH 066/191] 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 c3e5e817c9ec17..eab805c47144aa 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 0eed0ed4274962..ba9f75796276e0 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -242,14 +242,12 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 -- GitLab From b23b31f77799e47d1f08b492762c4023de2eac29 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 15 Nov 2016 12:28:48 -0600 Subject: [PATCH 067/191] 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 ba9f75796276e0..608e6f20748802 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -501,7 +501,9 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 -- GitLab From 598a0b328108b94c803f56db1103659fac69c6a2 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 15 Nov 2016 12:55:37 -0600 Subject: [PATCH 068/191] 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 608e6f20748802..cbb11b790ec7a4 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -3,9 +3,8 @@ 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 @@ def input_filtered_search(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') @@ -112,23 +115,31 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 @@ def expect_issues_list_count(open_count, closed_count = 0) input_filtered_search('bug') # Wait for search results to load - sleep 1 + sleep 2 end it 'open state' do -- GitLab From 561e2c99d9886ab71b3b3630ef5ec1f92957ed8e Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 30 Nov 2016 12:30:52 -0600 Subject: [PATCH 069/191] Refactor validTokenKeys --- .../filtered_search_manager.js.es6 | 51 +++------------- .../filtered_search_token_keys.js.es6 | 45 ++++++++++++++ .../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 8568bf78416412..3899181a3520d2 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 00000000000000..8d38a29a354286 --- /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 eab805c47144aa..b1f37443aa10fe 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, + }; } } -- GitLab From 873c2b66ba5a41116b6d54cbb2d88323efc28185 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 30 Nov 2016 12:32:10 -0600 Subject: [PATCH 070/191] Add static methods for dropdowns to interface with --- .../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 3899181a3520d2..09a7779635fa6e 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'); -- GitLab From dc7fefabd3dd0415a0c83b1b5fa33fbdefd1d385 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 30 Nov 2016 12:48:54 -0600 Subject: [PATCH 071/191] 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 5e759301a044be..4c27c835bee8b3 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 -- GitLab From 1b7bca6b757d7151892541687164be10e18a379c Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 30 Nov 2016 15:18:17 -0600 Subject: [PATCH 072/191] Add logic for dynamically selecting which dropdown to load [skip ci] --- .../filtered_search_manager.js.es6 | 55 +++++++++++++++---- .../filtered_search_tokenizer.es6 | 35 +++++++++--- .../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 09a7779635fa6e..8903f382c186e1 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 b1f37443aa10fe..b686a43cf32d03 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 4c27c835bee8b3..a45af053f5c710 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') -- GitLab From 7402aea7aab19a825990517840849637b2badaae Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 30 Nov 2016 15:25:10 -0600 Subject: [PATCH 073/191] 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 00000000000000..ebbd43ad8e095c --- /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 = {})); -- GitLab From 0e1761572fae57e5db0071366c4ca803f8097c45 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 2 Dec 2016 15:02:54 -0600 Subject: [PATCH 074/191] Add droplab updates --- app/assets/javascripts/application.js | 1 + app/assets/javascripts/droplab/droplab.js | 98 +++++++++++++++---- .../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 b7c4673c8e39f8..779cb2eb1d91a4 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -51,6 +51,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 18ca8be7203163..56582e71b61169 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 4a7ae0cbdc1a45..88e69c02422da5 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: [PATCH 075/191] 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_manager.js.es6 | 22 +++++- .../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 00000000000000..9e4d1018ac3e39 --- /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 ebbd43ad8e095c..0593561c8a1a46 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 00000000000000..250d8236ea9719 --- /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 8903f382c186e1..92f070243540b3 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 a45af053f5c710..04000a18dcef9c 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 -- GitLab From 25fd73c0793a69cc350624fa17f45b1f6b8efa53 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 2 Dec 2016 16:20:01 -0600 Subject: [PATCH 076/191] Update droplab --- app/assets/javascripts/droplab/droplab.js | 19 +++++++++++++++++-- .../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 56582e71b61169..aff47aa23cfcc9 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 23e43b352d632d..2dff5b83fae7a2 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); } }, }); -- GitLab From 18c8916fb3f3903cee419ba05309af9cac29f7bf Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 2 Dec 2016 16:20:21 -0600 Subject: [PATCH 077/191] Fix rendering of assignee dropdown after clicking hint dropdown --- .../javascripts/filtered_search/dropdown_assignee.js.es6 | 3 ++- .../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 9e4d1018ac3e39..e3cbb4cb3a0230 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 92f070243540b3..fc7bfe121fbace 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(); } } -- GitLab From b54c27272a3b8f52b08f280f93ddcef09d3d27db Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 2 Dec 2016 16:30:19 -0600 Subject: [PATCH 078/191] Add author dropdown --- .../filtered_search/dropdown_author.js.es6 | 22 +++++++++++++++++++ .../filtered_search_manager.js.es6 | 13 +++++++---- .../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 00000000000000..e16b313b7437dd --- /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 fc7bfe121fbace..237f4fc3fff5f2 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 04000a18dcef9c..3801b46a3326a2 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 -- GitLab From e8228193cec3354461f99acde07433568014b42e Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 2 Dec 2016 16:43:15 -0600 Subject: [PATCH 079/191] Add label and milestone dropdowns --- .../filtered_search/dropdown_label.js.es6 | 22 +++++++++++++++++++ .../filtered_search/dropdown_milestone.js.es6 | 22 +++++++++++++++++++ .../filtered_search_manager.js.es6 | 14 ++++++++++++ .../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 00000000000000..9225dca13b0ebe --- /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 00000000000000..ab97d709886f44 --- /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 237f4fc3fff5f2..f06d5a646cf942 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 3801b46a3326a2..cf5b1a523322f7 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 -- GitLab From 6435e782994947acf7342f4cbe300d5c80cd0a14 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 12:18:02 -0600 Subject: [PATCH 080/191] 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 aff47aa23cfcc9..0152eef793f32b 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 cf5b1a523322f7..c7841486ad1acf 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 -- GitLab From 78ac16c8f608151a41462430a7e2daff38bf64f7 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 12:26:36 -0600 Subject: [PATCH 081/191] Style hint dropdown --- .../filtered_search/dropdown_hint.js.es6 | 4 ++-- app/assets/stylesheets/framework/filters.scss | 22 +++++++++++++++++++ .../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 0593561c8a1a46..dc28b97fea9a92 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 c679a3833e9056..71b336461852b2 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 c7841486ad1acf..6df35b78194146 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 } -- GitLab From 216b1b6d6f4fbf100afa8f548ca64885184ae783 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 12:35:28 -0600 Subject: [PATCH 082/191] Style author dropdown --- .../filtered_search/dropdown_hint.js.es6 | 4 ++-- app/assets/stylesheets/framework/filters.scss | 11 ++++++++- .../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 dc28b97fea9a92..0593561c8a1a46 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 71b336461852b2..767803ac1d02fd 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 6df35b78194146..b83ea6c60d98e3 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}} -- GitLab From d187867f68a4084d025df2ddbc83c82e676365e2 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 13:00:18 -0600 Subject: [PATCH 083/191] 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 767803ac1d02fd..4fa826c1b76d24 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 18716813c48c10..58723f21c971e2 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -296,6 +296,11 @@ $dropdown-toggle-active-border-color: darken($dropdown-toggle-border-color, 14%) $dropdown-toggle-icon-color: #c4c4c4; $dropdown-toggle-hover-icon-color: darken($dropdown-toggle-icon-color, 7%); +/* +* Filtered Search +*/ +$dropdown-hover-color: #3B86FF; + /* * Buttons */ -- GitLab From e894b2bfa13378c93b4764e291a9ca211d14c0b8 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 13:00:54 -0600 Subject: [PATCH 084/191] Add static dropdown list items --- .../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 b83ea6c60d98e3..a47140ed0aa4f2 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 -- GitLab From 77f40ddaada3ad38d7e3b97553b4e2d16f966e6b Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 13:09:29 -0600 Subject: [PATCH 085/191] 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 4fa826c1b76d24..c4b4a56a8b5bd6 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 a47140ed0aa4f2..f076c9c1a75ec4 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 -- GitLab From 76a3d093d66066317bd41a53077dcf981748818d Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 13:17:43 -0600 Subject: [PATCH 086/191] Add vertical scrolling for dropdowns --- app/assets/stylesheets/framework/filters.scss | 9 +++++++ .../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 c4b4a56a8b5bd6..2efdb537cb3aac 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 f076c9c1a75ec4..8dda6e99d2d723 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}} -- GitLab From fafbe6dbf0f8d1bf2a5dfc4c14c97308839f00e7 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 13:26:24 -0600 Subject: [PATCH 087/191] 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 2efdb537cb3aac..1f980c3d618708 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; } -- GitLab From f0dfa9002addcd8b329ac3432fb486e685e1679a Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 13:32:44 -0600 Subject: [PATCH 088/191] 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 1f980c3d618708..bcbf0e868e2bfd 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; + } } } -- GitLab From e8e548459580a5eda2e7b9c8ed56ea118b2d5df3 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 13:34:20 -0600 Subject: [PATCH 089/191] 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 bcbf0e868e2bfd..130d06b601c877 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; -- GitLab From 82c935eb9ca1b48ed2fa62925ad5ea5d3a88f6a6 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 15:12:34 -0600 Subject: [PATCH 090/191] Add dropdown offset to match input cursor --- .../filtered_search_dropdown.js.es6 | 4 ++++ .../filtered_search_manager.js.es6 | 24 +++++++++++++++---- .../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 250d8236ea9719..251162f3fb1e14 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 f06d5a646cf942..c80c60d6d6eeae 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 ac44b81ee22e33..a6019afd29935b 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); }; -- GitLab From 95a5c486689626fce3d8bdec7e00a38431de1a04 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 15:40:55 -0600 Subject: [PATCH 091/191] Set data_dropdown_trigger to empty instead of removing --- .../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 251162f3fb1e14..0a406bef985f68 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(); -- GitLab From 390676669565db33dae0898ba45d7bb5ee4c899b Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 16:04:06 -0600 Subject: [PATCH 092/191] 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 0152eef793f32b..6befa0976d4f46 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() { -- GitLab From 751a17dedc8255d05ee572245e3bcb364450aa76 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 16:05:30 -0600 Subject: [PATCH 093/191] Add ability to click on none as an option --- .../filtered_search/dropdown_assignee.js.es6 | 8 +++++++- .../filtered_search/dropdown_hint.js.es6 | 5 ----- .../filtered_search/dropdown_label.js.es6 | 9 ++++++++- .../filtered_search/dropdown_milestone.js.es6 | 8 +++++++- .../filtered_search_dropdown.js.es6 | 19 ++++++++++++++++++- .../filtered_search_manager.js.es6 | 2 +- .../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 e3cbb4cb3a0230..fcaacac1b50b88 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 0593561c8a1a46..b7161d00eb9bdd 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 9225dca13b0ebe..ef92ecd3bd157e 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 ab97d709886f44..00df89ff063b9f 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 0a406bef985f68..a345b368238743 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 c80c60d6d6eeae..53ab2135a099c4 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 8dda6e99d2d723..39af0c2c288dc5 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 -- GitLab From 98228eac28a28857ce603aca6f4134eb3cf58703 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 16:29:55 -0600 Subject: [PATCH 094/191] Replace typed token with selected dropdown token --- .../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 53ab2135a099c4..7e6144b571deb0 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; } -- GitLab From e9ccecaebaaa17277872662febdd91b5cf0c5686 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 16:36:05 -0600 Subject: [PATCH 095/191] Populate selected item in filtered search input --- .../javascripts/filtered_search/dropdown_assignee.js.es6 | 3 ++- .../javascripts/filtered_search/dropdown_author.js.es6 | 5 ++++- .../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 fcaacac1b50b88..e791de5ad41915 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 e16b313b7437dd..75eb1c06fbd974 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 00df89ff063b9f..8c75bd30e97901 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(); -- GitLab From 145bb9d6c22d31167c6fb1bfdcdfbbf5aa01dbfb Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 6 Dec 2016 11:34:00 -0600 Subject: [PATCH 096/191] 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 130d06b601c877..0882af57482b4a 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 { -- GitLab From b285a08d134df9dbdc59ca97b048342c1006de56 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 6 Dec 2016 11:37:54 -0600 Subject: [PATCH 097/191] Remove unnecessary dismissCurrentDropdown --- .../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 7e6144b571deb0..c509a3c3b62c0e 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); -- GitLab From f31e116da9fc592b0d4539663160fd2ea6556309 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 6 Dec 2016 12:46:16 -0600 Subject: [PATCH 098/191] Fixed bug where labels with multiple spaces wouldn't get tokenized correctly --- .../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 b686a43cf32d03..17fdfe0f5501e0 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; } -- GitLab From 6038e91981a7c239bf437ba20ed53960e8f08a28 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 6 Dec 2016 12:52:23 -0600 Subject: [PATCH 099/191] Add escape quotations for selected labels from dropdown --- .../filtered_search/dropdown_label.js.es6 | 18 ++++++++++++++++-- .../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 ef92ecd3bd157e..cd1ccb541e6ec9 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 c509a3c3b62c0e..04374525d4c2c4 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; -- GitLab From d04722596412a22e404e271ca04710a09d416a1d Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 6 Dec 2016 12:57:07 -0600 Subject: [PATCH 100/191] Add escaping for milestone values --- .../filtered_search/dropdown_label.js.es6 | 17 ++--------------- .../filtered_search/dropdown_milestone.js.es6 | 3 ++- .../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 cd1ccb541e6ec9..d4a50422c3beec 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 8c75bd30e97901..965a8c8a58daca 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 a345b368238743..cc7f61c23e559e 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(); -- GitLab From 0c41328b8ca925c10608a92534de3e8acdd697b7 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 6 Dec 2016 15:13:31 -0600 Subject: [PATCH 101/191] Make keep typing dropdown item static --- .../filtered_search/dropdown_hint.js.es6 | 4 ---- .../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 b7161d00eb9bdd..b09136586c8b90 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 39af0c2c288dc5..0a5de59cb639a1 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 -- GitLab From 4de2a0bea5029d8e19adb02d14e94e5ab0be0194 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 6 Dec 2016 16:12:00 -0600 Subject: [PATCH 102/191] Add filter by last token --- .../javascripts/droplab/droplab_filter.js | 13 ++++- .../filtered_search/dropdown_author.js.es6 | 8 +++ .../filtered_search_dropdown.js.es6 | 13 +++-- .../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 88e69c02422da5..5ae81afaf89683 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 75eb1c06fbd974..64c310ba7ad5c6 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 cc7f61c23e559e..bbfe26e6a2118a 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 17fdfe0f5501e0..d6df83a3fb9c11 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 = ''; -- GitLab From 56002661bdb40f4bf0a55d58b63d97f7325af992 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 6 Dec 2016 16:20:14 -0600 Subject: [PATCH 103/191] Add filtering to the remaining dropdowns --- .../javascripts/filtered_search/dropdown_label.js.es6 | 8 ++++++++ .../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 d4a50422c3beec..c5493f7a8872eb 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 965a8c8a58daca..8317ce5824c35a 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; -- GitLab From 925acf3d21c22b58d927fbf192e97dd2f504b237 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 6 Dec 2016 16:23:04 -0600 Subject: [PATCH 104/191] Add filterMethod to hint dropdown --- .../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 b09136586c8b90..0bee2eb29861ab 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; -- GitLab From ce04150c76cea2e8253d036bd7d13d0827395e7c Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 7 Dec 2016 12:33:02 -0600 Subject: [PATCH 105/191] 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 04374525d4c2c4..7e399427cef8d0 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) { -- GitLab From 6c5702ac19b49c776d0ac075f6a9eb1e312ea631 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 7 Dec 2016 12:33:33 -0600 Subject: [PATCH 106/191] 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 0882af57482b4a..205cecb4906005 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; -- GitLab From 8e888e22d6b5222abaefb1c5f19fbc5c84dbf400 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 7 Dec 2016 12:51:25 -0600 Subject: [PATCH 107/191] Add ability to search for filter dropdowns without filter symbol --- .../filtered_search/dropdown_assignee.js.es6 | 15 +++++++++++++++ .../filtered_search/dropdown_author.js.es6 | 11 +++++++++-- .../filtered_search/dropdown_label.js.es6 | 8 ++++++-- .../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 e791de5ad41915..63fbe30ee8469a 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 64c310ba7ad5c6..37e2e80533b9bc 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 c5493f7a8872eb..e2c1305597a883 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 8317ce5824c35a..cd185d319174dc 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; } } -- GitLab From ed1b012ca5b65549280f3226bc994ff64cd2edc4 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 7 Dec 2016 12:54:29 -0600 Subject: [PATCH 108/191] 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 6befa0976d4f46..84cd89297ff87f 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; -- GitLab From 74614e80d97eab2faaeaa3e6ce197859d12adfb3 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 7 Dec 2016 12:55:03 -0600 Subject: [PATCH 109/191] Reset filters after clear search --- .../filtered_search_dropdown.js.es6 | 38 ++++++++++++++++++- .../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 bbfe26e6a2118a..edffd7fb8e2b90 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 7e399427cef8d0..841738ff62789d 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); - } } -- GitLab From 81483187e456ecc539064eaf2863514d0edc4691 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 8 Dec 2016 15:36:54 -0600 Subject: [PATCH 110/191] Update droplab --- app/assets/javascripts/droplab/droplab.js | 190 ++++++++++++------ .../javascripts/droplab/droplab_ajax.js | 89 ++++---- .../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_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 84cd89297ff87f..4d83b609a739f0 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 2dff5b83fae7a2..b81663c281d880 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 37e2e80533b9bc..c02f1e25407271 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 0bee2eb29861ab..481faa7fd49337 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 e2c1305597a883..af47ad2a1f8ce8 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 cd185d319174dc..9810767eb66256 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 edffd7fb8e2b90..80c3407b7faedf 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 841738ff62789d..af8e145fa7f978 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() { -- GitLab From af544b904938e1e802b18b9204e9586194dd837d Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 8 Dec 2016 15:42:58 -0600 Subject: [PATCH 111/191] Fix turbolinks issue by cleaning up droplab on page:change --- .../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 af8e145fa7f978..f28ce6b4366a54 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) { -- GitLab From b2784e4007e09c40ffde040a4a0d9f8341583906 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 8 Dec 2016 16:40:46 -0600 Subject: [PATCH 112/191] Add basic ajax filtering for author --- .../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 00000000000000..b346f22f1c2d89 --- /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 c02f1e25407271..cb3a6b6ab6df22 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; + } } } -- GitLab From 19ff891bdaf5a1d96408d03831c41ed96f229765 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 9 Dec 2016 11:44:09 -0600 Subject: [PATCH 113/191] Code cleanup --- app/assets/javascripts/droplab/droplab.js | 11 +- .../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_manager.js.es6 | 194 ++++++++---------- .../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 4d83b609a739f0..b17f156acb44b5 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 b346f22f1c2d89..c345fda1075d03 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 7609546a3a60d5..b2b03b637e7d39 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 cb3a6b6ab6df22..9bd49ab1a7877b 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 481faa7fd49337..f885267880ae98 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 af47ad2a1f8ce8..24a795808cadce 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 9810767eb66256..458a9b1c5c1e1e 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 80c3407b7faedf..2f92c7b2e2a282 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 f28ce6b4366a54..9846f3ba50dfd3 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 0a5de59cb639a1..53983ef8d6d1e5 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 -- GitLab From 8b4a79801bb5f02551521646a175e4ee60e71efc Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 9 Dec 2016 12:11:29 -0600 Subject: [PATCH 114/191] Fixed issue where dropdown would not open after clicking on a dropdown item --- app/assets/javascripts/droplab/droplab.js | 3 +-- .../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 b17f156acb44b5..6b3263380508e1 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 53983ef8d6d1e5..2d2ecf030a858d 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}}'} -- GitLab From b7f9ca1350117e94302c2871b6253e5d9ff6012e Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 9 Dec 2016 13:15:09 -0600 Subject: [PATCH 115/191] Fix bug where dropdowns would not dismiss properly --- .../javascripts/filtered_search/dropdown_assignee.js.es6 | 2 +- .../javascripts/filtered_search/dropdown_hint.js.es6 | 9 ++++++--- .../javascripts/filtered_search/dropdown_label.js.es6 | 3 ++- .../filtered_search/dropdown_milestone.js.es6 | 2 +- .../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 b2b03b637e7d39..850cca670e4624 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 f885267880ae98..ea384af09a9981 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 24a795808cadce..c79df0aee4ac57 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 458a9b1c5c1e1e..10535097747abc 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 2f92c7b2e2a282..cd46e430e012e1 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(); } } -- GitLab From e610e5e098bafbc4930d6cbb76aac6c2e9138277 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 9 Dec 2016 13:15:38 -0600 Subject: [PATCH 116/191] 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 d6df83a3fb9c11..5ad433f4a097aa 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; -- GitLab From c25a79dbee9750981c9c989367831bef78412a56 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 9 Dec 2016 13:17:19 -0600 Subject: [PATCH 117/191] 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_manager.js.es6 | 17 +++++-- .../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 6b3263380508e1..42ddb7a4a56a29 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 850cca670e4624..3420397edda2f9 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 9bd49ab1a7877b..f1401f6f9d2779 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 c79df0aee4ac57..4c9926c1f7894d 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 10535097747abc..33967ddff24eeb 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 cd46e430e012e1..163dac65842b32 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 9846f3ba50dfd3..4f5d144bff34a9 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 5ad433f4a097aa..4abb5e94d81649 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) { -- GitLab From d53a47c239ae903d75aa0834bc7ac5d8effc0803 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 9 Dec 2016 15:10:15 -0600 Subject: [PATCH 118/191] 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 c816b616631c79..a112928c6dedfb 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -244,7 +244,9 @@ def page_filter_path(options = {}) 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] } -- GitLab From 3745218834f7e71fa3eee8276feef878fcadca1e Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 9 Dec 2016 15:20:41 -0600 Subject: [PATCH 119/191] Prevent droplab from opening dropdown by cleaning it --- .../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 4f5d144bff34a9..91de7783cc1309 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(); } } -- GitLab From cce2074fec28e6bf283e8940515be15a0a8c0ee9 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 9 Dec 2016 15:36:44 -0600 Subject: [PATCH 120/191] Reposition dropdown when backspace is hit --- .../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 91de7783cc1309..d21ae70cdb9631 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) { -- GitLab From 8408f0fba7403d0ee2ecb76133ad0fa6267c34da Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 9 Dec 2016 15:47:56 -0600 Subject: [PATCH 121/191] Fix clear button so that it resets the dropdowns properly --- .../filtered_search/filtered_search_dropdown.js.es6 | 4 ++++ .../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 163dac65842b32..03835b6522b560 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 d21ae70cdb9631..0654d7d816a11a 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) { -- GitLab From e05d69a0f8273a37f7e4036cf1f6cb975e9a513f Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 9 Dec 2016 16:02:43 -0600 Subject: [PATCH 122/191] Reset filters when clear search is clicked --- .../filtered_search/filtered_search_dropdown.js.es6 | 9 +++++++++ .../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 03835b6522b560..5186c15cb67eac 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 0654d7d816a11a..c7e01fc710d13b 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(); -- GitLab From 03005495caf78c1f54ab1637d22b104476b5b2da Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 11:13:17 -0600 Subject: [PATCH 123/191] 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 42ddb7a4a56a29..359cd82bbcde45 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 c345fda1075d03..0d6a7892bdc50b 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); } -- GitLab From ce6003df313ae6d0bdf6b8b7b70e06c29a30cfb7 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 13:35:39 -0600 Subject: [PATCH 124/191] 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 0d6a7892bdc50b..7603556d2ef4a0 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; -- GitLab From 2233eb93a83d215d29df6159331fea12d2d49ecd Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 13:47:36 -0600 Subject: [PATCH 125/191] Pass project ID through the DOM --- .../javascripts/filtered_search/dropdown_assignee.js.es6 | 2 +- app/assets/javascripts/filtered_search/dropdown_author.js.es6 | 2 +- .../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 3420397edda2f9..ff3fd3a4e2b284 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 f1401f6f9d2779..517cbab8ee732c 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 5186c15cb67eac..478c4e6bf92fbd 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 2d2ecf030a858d..86692e776971ea 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') -- GitLab From c013e8bb805622da86db556d5500b49d029c64ee Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 13:55:41 -0600 Subject: [PATCH 126/191] Fixed bug where filters were not being reset after being cleared --- .../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 c7e01fc710d13b..5d38a23d9fd111 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); } -- GitLab From 0970430e9fbe05fd46d574a3f35d501711dd1bc4 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 14:04:16 -0600 Subject: [PATCH 127/191] Add missing space for extracting params --- .../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 5d38a23d9fd111..055f229cd45397 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 + -- GitLab From a7d17104c4ada06614c666d3f5dcdad19e5f012f Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 14:14:21 -0600 Subject: [PATCH 128/191] Make ajax filter more consistent and only filter when typed --- .../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 7603556d2ef4a0..f2720a0371bfb0 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(); } -- GitLab From 2f1a1013f8963e1dc74e16cf59d117a49a08c49e Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 15:37:26 -0600 Subject: [PATCH 129/191] Add loading message for droplab_ajax --- app/assets/javascripts/droplab/droplab_ajax.js | 15 +++++++++++++++ .../filtered_search/dropdown_label.js.es6 | 4 ++++ .../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 b81663c281d880..629260006f3e9b 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 4c9926c1f7894d..0912336b6cf2ef 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 33967ddff24eeb..73d675738688a1 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 205cecb4906005..b6c137d647a0e3 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; +} -- GitLab From 9f7146bc3210b32f860a81a278abe6bcb687bb13 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 15:37:47 -0600 Subject: [PATCH 130/191] Add loading template to droplab_ajax_filter --- .../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 f2720a0371bfb0..8d024c4b6d7612 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 ff3fd3a4e2b284..edc717304b2f44 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 517cbab8ee732c..8d95a879c79a14 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: ` +
+ +
`, } }; } -- GitLab From ed78fb500a6410dcf71d2c3ed273083dd45cef0f Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 15:40:12 -0600 Subject: [PATCH 131/191] Refactor loadingTemplate to abstract class --- .../javascripts/filtered_search/dropdown_assignee.js.es6 | 5 +---- .../javascripts/filtered_search/dropdown_author.js.es6 | 5 +---- app/assets/javascripts/filtered_search/dropdown_label.js.es6 | 5 +---- .../javascripts/filtered_search/dropdown_milestone.js.es6 | 5 +---- .../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 edc717304b2f44..0ce4eebedc9a89 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 8d95a879c79a14..3dc649cc17dba9 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 0912336b6cf2ef..bf009454de5e30 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 73d675738688a1..7f5822aed84f55 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 478c4e6bf92fbd..6b713a7017e172 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(); } -- GitLab From 1830fe0612ff9e5c433759aa24b2302d871a6309 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 16:04:58 -0600 Subject: [PATCH 132/191] 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 8d024c4b6d7612..bdd9b059bb3066 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(); }); -- GitLab From b14cde0a65c93a52906d1ed514a56518485f9a0b Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 16:06:45 -0600 Subject: [PATCH 133/191] 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 bdd9b059bb3066..943ee9fa0a463f 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]); } -- GitLab From 5b021c4416870d670f63c49ab708444597d20d61 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 18:42:20 -0600 Subject: [PATCH 134/191] 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 86692e776971ea..7ebc4d6b15362f 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 -- GitLab From 88b99b9e49a68ae58a315f2b0970b0866f4a6a77 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 18:53:56 -0600 Subject: [PATCH 135/191] 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 abfdd7a759d1e4..b98070925535b7 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 7ebc4d6b15362f..f7c72e3ced8c2f 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] -- GitLab From 1f40f1e6fd69491217660140d6410ded2adf7e26 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sun, 11 Dec 2016 18:05:33 -0600 Subject: [PATCH 136/191] 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 629260006f3e9b..ebb518eeef4695 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) { -- GitLab From b2c2fdf98ac337a3b13c294e5e113da795eed25f Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sun, 11 Dec 2016 18:05:55 -0600 Subject: [PATCH 137/191] Refactor dropdown filters --- .../filtered_search/dropdown_author.js.es6 | 57 ------------------- .../filtered_search/dropdown_hint.js.es6 | 2 +- .../filtered_search/dropdown_label.js.es6 | 45 --------------- ...estone.js.es6 => dropdown_non_user.js.es6} | 18 +++--- ...n_assignee.js.es6 => dropdown_user.js.es6} | 14 ++--- .../filtered_search_manager.js.es6 | 16 ++++-- .../shared/issuable/_search_bar.html.haml | 4 +- 7 files changed, 28 insertions(+), 128 deletions(-) delete mode 100644 app/assets/javascripts/filtered_search/dropdown_author.js.es6 delete mode 100644 app/assets/javascripts/filtered_search/dropdown_label.js.es6 rename app/assets/javascripts/filtered_search/{dropdown_milestone.js.es6 => dropdown_non_user.js.es6} (65%) rename app/assets/javascripts/filtered_search/{dropdown_assignee.js.es6 => dropdown_user.js.es6} (85%) 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 3dc649cc17dba9..00000000000000 --- 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 ea384af09a9981..d445a796f435d6 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 bf009454de5e30..00000000000000 --- 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_non_user.js.es6 similarity index 65% rename from app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 rename to app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 index 7f5822aed84f55..05c9284bc96949 100644 --- a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 @@ -2,18 +2,18 @@ /*= require filtered_search/filtered_search_dropdown */ ((global) => { - class DropdownMilestone extends gl.FilteredSearchDropdown { - constructor(droplab, dropdown, input) { + class DropdownNonUser extends gl.FilteredSearchDropdown { + constructor(droplab, dropdown, input, endpoint, symbol) { super(droplab, dropdown, input); - this.listId = 'js-dropdown-milestone'; + this.listId = dropdown.id; this.config = { droplabAjax: { - endpoint: 'milestones.json', + endpoint: endpoint, method: 'setData', loadingTemplate: this.loadingTemplate, }, droplabFilter: { - filterFunction: this.filterWithSymbol.bind(this, '%'), + filterFunction: this.filterWithSymbol.bind(this, symbol), } }; } @@ -22,9 +22,9 @@ 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)); + 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); @@ -40,5 +40,5 @@ } } - global.DropdownMilestone = DropdownMilestone; + global.DropdownNonUser = DropdownNonUser; })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 similarity index 85% rename from app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 rename to app/assets/javascripts/filtered_search/dropdown_user.js.es6 index 0ce4eebedc9a89..1a597bbbc9dc34 100644 --- a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 @@ -2,10 +2,10 @@ /*= require filtered_search/filtered_search_dropdown */ ((global) => { - class DropdownAssignee extends gl.FilteredSearchDropdown { + class DropdownUser extends gl.FilteredSearchDropdown { constructor(droplab, dropdown, input) { super(droplab, dropdown, input); - this.listId = 'js-dropdown-assignee'; + this.listId = dropdown.id; this.config = { droplabAjaxFilter: { endpoint: '/autocomplete/users.json', @@ -18,7 +18,7 @@ }, searchValueFunction: this.getSearchInput, loadingTemplate: this.loadingTemplate, - } + }, }; } @@ -45,11 +45,7 @@ const hasPrefix = valueWithoutColon[0] === '@'; const valueWithoutPrefix = valueWithoutColon.slice(1); - if (hasPrefix) { - return valueWithoutPrefix; - } else { - return valueWithoutColon; - } + return hasPrefix ? valueWithoutPrefix : valueWithoutColon; } configure() { @@ -57,5 +53,5 @@ } } - global.DropdownAssignee = DropdownAssignee; + 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 055f229cd45397..c92d669114efc3 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 f7c72e3ced8c2f..335552c0a26a6f 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 -- GitLab From 04fb53056347ed4b20175c34e4aa814d3dc9f0f6 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sun, 11 Dec 2016 18:11:58 -0600 Subject: [PATCH 138/191] 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 05c9284bc96949..f03c27c3ec02fc 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 1a597bbbc9dc34..6827ab1658a8d3 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 6b713a7017e172..c63ba1acf0bf18 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(); } -- GitLab From 12311df37156249c2532ecc28dd5e211e5afce71 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 09:21:38 -0600 Subject: [PATCH 139/191] 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_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 d445a796f435d6..53952e6bc6362f 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 f03c27c3ec02fc..e4df39cfde1231 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 6827ab1658a8d3..d3c3be9b9145ee 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 c63ba1acf0bf18..38ecbbf552df07 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 00000000000000..67a474985c0a49 --- /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 c92d669114efc3..d9ea44b3a1335c 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(); } -- GitLab From 0462ffea5ae3710ac4c0845399fda44979c00b98 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 09:47:21 -0600 Subject: [PATCH 140/191] 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 943ee9fa0a463f..6e1eb080e3bbfe 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); -- GitLab From 75f58735657b8a82571fff70b517d36a50551c09 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 09:55:27 -0600 Subject: [PATCH 141/191] Refactor itemClicked --- .../filtered_search/dropdown_non_user.js.es6 | 11 +++-------- .../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 e4df39cfde1231..752a9a6e242a12 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 d3c3be9b9145ee..749fb9d90aa633 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 38ecbbf552df07..990d56188cb578 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) { -- GitLab From bbcd9a0a5a0feb337b99646b815606c6c4ac4eb4 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 10:28:22 -0600 Subject: [PATCH 142/191] 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_manager.js.es6 | 9 ++-- .../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 53952e6bc6362f..43a0b1da0fea99 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 752a9a6e242a12..0969df658363ff 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 749fb9d90aa633..8bc274e0b129a0 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 990d56188cb578..85d684e3058213 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 67a474985c0a49..a0764c275e5f02 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 d9ea44b3a1335c..d3bccc4b14cfef 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 4abb5e94d81649..ac45d3b798647f 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, -- GitLab From 5c618a63b2daca35be5c7c17adfb13d22132b75f Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 10:55:43 -0600 Subject: [PATCH 143/191] Fix bug where labels with spaces weren't being escaped when selected --- .../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 0969df658363ff..84abaa920d678a 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}"`; } } -- GitLab From 08797d99dc4c9319cc93cb8576618b07d13480be Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 10:55:55 -0600 Subject: [PATCH 144/191] 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 85d684e3058213..a9dbb0f7ccbd63 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(); -- GitLab From d4564402d3a3d2c2561aa916d3e6006170af52ce Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 11:06:45 -0600 Subject: [PATCH 145/191] Add comments to resolve todos --- .../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 d3bccc4b14cfef..14e2e698f93eeb 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); -- GitLab From 579feedd909070bd610816a757ab983cbca663c7 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 11:08:15 -0600 Subject: [PATCH 146/191] 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 6e1eb080e3bbfe..c6c062d08863ba 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) { -- GitLab From a316ed577b4db424244b006f3b6ba11fdd4d3371 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 11:19:44 -0600 Subject: [PATCH 147/191] Fix missing method from refactoring --- .../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 43a0b1da0fea99..1aef27163c6537 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); -- GitLab From 2993e9cc475e07995253f0980ceb57ba7be9b386 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 16:25:31 -0600 Subject: [PATCH 148/191] Fix code styling issues --- .../filtered_search/dropdown_hint.js.es6 | 2 +- .../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 1aef27163c6537..a79779e4977903 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 a9dbb0f7ccbd63..130e6bba3417a8 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 a0764c275e5f02..59166840c50f89 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(); } -- GitLab From 34333122e8a38f2602b7c764635a79655b5c6098 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 16:27:10 -0600 Subject: [PATCH 149/191] Add support for delete key --- .../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 14e2e698f93eeb..ebbd7e3129efa8 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(); } -- GitLab From 4784a6c326e4716a201df7a44b56d4adeba8b644 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 16:28:17 -0600 Subject: [PATCH 150/191] Remove unnecessary stopPropagation --- .../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 ebbd7e3129efa8..e068b5d2ebfa86 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 -- GitLab From d9226f96c3a172103ec7dd204b9d68c3f7ebadb4 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 16:29:51 -0600 Subject: [PATCH 151/191] Fix regex for + --- .../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 e068b5d2ebfa86..a89627384e9b24 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) { -- GitLab From 65f7b66a9a803717a2fa70b8d1971d266945150a Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 16:31:22 -0600 Subject: [PATCH 152/191] Reduce over-verboseness --- .../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 a89627384e9b24..77a9de96c8a650 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}`; -- GitLab From 873f0bfcbeb2a509077e23610f4c3ee662015972 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 16:34:01 -0600 Subject: [PATCH 153/191] 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 a6019afd29935b..d6c1512effffc3 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; -- GitLab From 09d40468cdeb11fc0b430d835f80160f7676a0d0 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 16:37:49 -0600 Subject: [PATCH 154/191] Move functions into class --- .../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 77a9de96c8a650..00b7dc195bb355 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=✓'; -- GitLab From 2b7251efaf45fa47b39dde11147bb8f28e828843 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 16:42:00 -0600 Subject: [PATCH 155/191] Convert string concatenations with an array join --- .../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 00b7dc195bb355..d0e39b6390dc1d 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('&')}`; } } -- GitLab From 60b26c5ca4bc941ed999d7dfed21b94b57337908 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 21:07:42 -0600 Subject: [PATCH 156/191] Refactor static data to get information from other variables instead --- app/assets/javascripts/filtered_search/dropdown_user.js.es6 | 4 ++-- .../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 8bc274e0b129a0..69b1ec3ea041cf 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 130e6bba3417a8..a5d8b0969c67af 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 = `
-- GitLab From 1f0facd9ceda65e22f66d9f65e9bdf116f07f843 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 21:15:50 -0600 Subject: [PATCH 157/191] Add getParameterByName --- .../filtered_search_manager.js.es6 | 18 ++---------------- .../javascripts/lib/utils/common_utils.js | 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 d0e39b6390dc1d..2237a21ca6056b 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 b/app/assets/javascripts/lib/utils/common_utils.js index 8fa80502d922df..7169db66eb8a97 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -110,6 +110,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; }; -- GitLab From 93a4902d4ee599176efc58a86079a2fb0652fd5b Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 21:19:16 -0600 Subject: [PATCH 158/191] Use turbolinks instead of window.location --- .../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 2237a21ca6056b..e087d0fd45bb30 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('&')}`); } } -- GitLab From 875f3602215cd58a61e84cccc02aa618a2acccc8 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 21:24:55 -0600 Subject: [PATCH 159/191] Refactor getUrlParamsArray() --- .../filtered_search/filtered_search_manager.js.es6 | 4 +--- app/assets/javascripts/lib/utils/common_utils.js | 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 e087d0fd45bb30..3e57215d608a37 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 b/app/assets/javascripts/lib/utils/common_utils.js index 7169db66eb8a97..b54b4673bdba90 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -110,6 +110,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, '\\$&'); -- GitLab From 6f9b7ebe0f68b459e73bb821db9edc4e13cbdfa6 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 22:15:31 -0600 Subject: [PATCH 160/191] Refactor FilteredSearchTokenKeys model --- .../filtered_search_dropdown_manager.js.es6 | 2 +- .../filtered_search_manager.js.es6 | 48 ++++----- .../filtered_search_token_keys.js.es6 | 97 ++++++++++++------- .../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 59166840c50f89..682857d1899828 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 3e57215d608a37..d7fb3a0c2049c4 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 8d38a29a354286..97eab6be8df3f9 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 ac45d3b798647f..365171252a16df 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; -- GitLab From 239da48e82903c963b34705322e9ae557fda264b Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 23:16:45 -0600 Subject: [PATCH 161/191] Simplify if else to make code easier to understand --- .../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 d7fb3a0c2049c4..87bcbd272cae76 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); -- GitLab From d5edd5dedf1ce8094a59b226a5d0df7226e6eff9 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 13 Dec 2016 08:51:49 -0600 Subject: [PATCH 162/191] 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 65ae17c93a75eb..bf2629942534c9 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -35,7 +35,7 @@ break; case 'projects:merge_requests:index': case 'projects:issues:index': - if(gl.hasOwnProperty('FilteredSearchManager')) { + if(gl.FilteredSearchManager) { new gl.FilteredSearchManager(); } Issuable.init(); -- GitLab From 896471ee7f527ded7061d1823537e6021a7dd271 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 13 Dec 2016 08:52:43 -0600 Subject: [PATCH 163/191] 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_manager.js.es6 | 15 ++++----- .../filtered_search_token_keys.js.es6 | 11 ++++--- .../filtered_search_tokenizer.es6 | 8 ++--- .../javascripts/lib/utils/common_utils.js | 2 +- .../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 a79779e4977903..b920b17d9152e8 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 84abaa920d678a..95133db4c04870 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 69b1ec3ea041cf..2ee46559e63725 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 a5d8b0969c67af..7ddfdca10fa3e2 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 682857d1899828..7864ebf7aa1761 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 87bcbd272cae76..96131a673eff38 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 97eab6be8df3f9..a1830d13e5f566 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 365171252a16df..0507f7bbc480f8 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 b/app/assets/javascripts/lib/utils/common_utils.js index b54b4673bdba90..c12fc6b5bd3e04 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -114,7 +114,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 d6c1512effffc3..1d5273621217b7 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); -- GitLab From acfd2c28f30e896dfe57eeee8edd0ae3385b0b8e Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 13 Dec 2016 09:52:23 -0600 Subject: [PATCH 164/191] Rename to .js.es6 --- ...ered_search_tokenizer.es6 => filtered_search_tokenizer.js.es6} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/assets/javascripts/filtered_search/{filtered_search_tokenizer.es6 => filtered_search_tokenizer.js.es6} (100%) diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 similarity index 100% rename from app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 rename to app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 -- GitLab From d176a3963a312c49b1fd9e57d465715e6dfd026b Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 13 Dec 2016 21:36:54 -0600 Subject: [PATCH 165/191] Fix es6 errors --- app/assets/javascripts/droplab/droplab.js | 101 ++++++++++++------ .../droplab/droplab_ajax_filter.js | 2 +- .../javascripts/lib/utils/common_utils.js | 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 359cd82bbcde45..94236153e41818 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: [PATCH 166/191] 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_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 94236153e41818..ed545ec8748629 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 95133db4c04870..54090375c5c214 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 2ee46559e63725..7a5669073120fc 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 7ddfdca10fa3e2..6c66a3b0613cbd 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 7864ebf7aa1761..ac71b5e4434244 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 96131a673eff38..e5b37f1e6910be 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 0507f7bbc480f8..57c0e8fc359ca1 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(); -- GitLab From 6cc08e3f2037b904139ce34229e00961890adb25 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 13 Dec 2016 21:59:28 -0600 Subject: [PATCH 167/191] 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 b6c137d647a0e3..dbe94813a935ce 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 58723f21c971e2..27f35301a26961 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -299,7 +299,7 @@ $dropdown-toggle-hover-icon-color: darken($dropdown-toggle-icon-color, 7%); /* * Filtered Search */ -$dropdown-hover-color: #3B86FF; +$dropdown-hover-color: #3b86ff; /* * Buttons -- GitLab From a68a70256a48560b5e93a492ea0af7d086206753 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 13 Dec 2016 23:30:28 -0600 Subject: [PATCH 168/191] Add jasmine tests to dropdown utils --- .../filtered_search/dropdown_utils.js.es6 | 68 ++++++++++ .../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 00000000000000..3837b020fd3453 --- /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 00000000000000..07293b9f877385 --- /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); + }); + }); + }); +})(); -- GitLab From e305f304d26c5087e3bd202498f6d0f8cc6f3902 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 14 Dec 2016 13:39:03 -0600 Subject: [PATCH 169/191] 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 dbe94813a935ce..e47511940a72f2 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; } } -- GitLab From 7395716c355cb808ca0587d061d1caa2b82b00af Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 14 Dec 2016 13:52:11 -0600 Subject: [PATCH 170/191] Fix HAML attributes --- .../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 335552c0a26a6f..dbef87e67cfa06 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 -- GitLab From bd948db88bc78c4e3529b1035d9c01e3c3a29a40 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 14 Dec 2016 14:17:28 -0600 Subject: [PATCH 171/191] Add tests for new common_utils functions --- .../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 ef75f6008985aa..4ba83d235c4a9a 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); + }); + }); + }); })(); -- GitLab From 573488945be58738727f6a19ff2c7dfb00361071 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 14 Dec 2016 14:37:17 -0600 Subject: [PATCH 172/191] Add text utility spec --- .../lib/utils/common_utils_spec.js.es6 | 1 - .../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 4ba83d235c4a9a..031f9ca03c9636 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 00000000000000..e97356b65d5510 --- /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); + }); + }); + }); +})(); -- GitLab From 82b72b28fcb0f40a05a9a40f7465053afd9c44e4 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 14 Dec 2016 15:34:04 -0600 Subject: [PATCH 173/191] 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 dbef87e67cfa06..aca39941381845 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 -- GitLab From 93643242d9f2d3c917c71845f3a05e661166056f Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 14 Dec 2016 21:51:33 -0600 Subject: [PATCH 174/191] Add jasmine tests for filtered search dropdown manager --- ...ltered_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 00000000000000..11765d7d7eac9d --- /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'); + }); + }); + }); + }); +})(); -- GitLab From d78bed3e0166200b346e31d6ee7519c2c5ff977e Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 08:34:34 -0600 Subject: [PATCH 175/191] 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 1b7f642d59e0af..e2d548f95e97f5 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 + "']"; -- GitLab From d39cfa35ed9c66d380b95d48e69d73be9c6f416a Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 08:40:25 -0600 Subject: [PATCH 176/191] Fix eslint --- .../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 11765d7d7eac9d..4a358bd43e3dbb 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'); -- GitLab From 2ffe83f12bee818220ed1f04c024bd3b410e3bb5 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 09:07:34 -0600 Subject: [PATCH 177/191] 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 bf2629942534c9..5d4262bc341f04 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -35,7 +35,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 49d7a3b9af248d..00000000000000 --- 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 80670063ea00e0..b2b4fe722205ec 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. -- GitLab From fda6d3f8a2e9af8d9c2160b94e0ea60987ece47d Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 11:20:05 -0600 Subject: [PATCH 178/191] 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 e47511940a72f2..8b7cb2454207ef 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; } } } -- GitLab From 8af6eabdafa0e325167b256bc17e9f236bf10a86 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 12:19:03 -0600 Subject: [PATCH 179/191] Add specs for addWordToInput --- ...ltered_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 4a358bd43e3dbb..17d414aaad1a70 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"\''); }); }); }); -- GitLab From bb51ecdb8e10ce05cbac1af25aa24b7df2de46b5 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 12:54:24 -0600 Subject: [PATCH 180/191] 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 a1830d13e5f566..6bd9cb06362678 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 00000000000000..6df7c0e44efe9e --- /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]); + }); + }); + }); +})(); -- GitLab From 0b4a85a93fe98be908ae9fabcb03359b7654ce40 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 14:16:29 -0600 Subject: [PATCH 181/191] 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 00000000000000..c93f163e763c47 --- /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); + }); + }); + }); + }); +})(); -- GitLab From 0e3b409d624bd31a0218e49cc208b2e8a11d554d Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 15:10:45 -0600 Subject: [PATCH 182/191] 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 9a7079848a5eb4..a05b83959fb703 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -170,7 +170,7 @@ 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 @@ 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 -- GitLab From d21f6d55953fedc2a087d8ee4ec256dc1d167d7f Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 15:57:35 -0600 Subject: [PATCH 183/191] 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 cbb11b790ec7a4..391c8905630efd 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -3,8 +3,8 @@ 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 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 -- GitLab From 21c8a381fd644f34e6ee8aa670be1874a8b9e085 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 16:11:37 -0600 Subject: [PATCH 184/191] 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 aca39941381845..896769768eba38 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 -- GitLab From 2f37b1b921531d882f51afa516c56d3797bdef6c Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 16:11:45 -0600 Subject: [PATCH 185/191] 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 1d632671fe2afd..d0abdc284eae99 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') -- GitLab From 889f15fd99c2f36ed692cf2364940bcf9fda607f Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 16:18:16 -0600 Subject: [PATCH 186/191] 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 391c8905630efd..8911b919cf7ca7 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -654,9 +654,9 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 @@ def expect_issues_list_count(open_count, closed_count = 0) 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]) -- GitLab From deae6938e5eaa4301751d40599f95b6091516d91 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 18:53:51 -0600 Subject: [PATCH 187/191] 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 8911b919cf7ca7..0b94bcc4e3f931 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -67,19 +67,19 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 @@ def expect_issues_list_count(open_count, closed_count = 0) 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 -- GitLab From a5d50d07437e31a0f40ac9ea2e8ceff1a5116515 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 23:08:49 -0600 Subject: [PATCH 188/191] Fix dropdown hint reset when changing tabs --- .../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 34079b258463ce..b9f552b62b959f 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() { -- GitLab From fb15de5af764ca052319da1f9b8670025c546773 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 23:20:31 -0600 Subject: [PATCH 189/191] Add selected tagName check for itemClicked --- .../filtered_search/dropdown_hint.js.es6 | 20 ++++++++++--------- .../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 b9f552b62b959f..bdcece619844b4 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 6c66a3b0613cbd..68014e27462012 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() { -- GitLab From e1009f021baa373547a7cf318d8cdfe270cfcbb7 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 16 Dec 2016 01:05:27 -0600 Subject: [PATCH 190/191] Create dropdown hint spec --- .../filtered_search/dropdown_hint_spec.rb | 113 ++++++++++++++++++ .../filter_issues_spec.rb | 12 +- .../{ => filtered_search}/search_bar_spec.rb | 33 +++++ 3 files changed, 152 insertions(+), 6 deletions(-) create mode 100644 spec/features/issues/filtered_search/dropdown_hint_spec.rb rename spec/features/issues/{ => filtered_search}/filter_issues_spec.rb (98%) rename spec/features/issues/{ => filtered_search}/search_bar_spec.rb (52%) 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 00000000000000..364d4bf4db14ed --- /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/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb similarity index 98% rename from spec/features/issues/filter_issues_spec.rb rename to spec/features/issues/filtered_search/filter_issues_spec.rb index 0b94bcc4e3f931..283814d2cbb9c3 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -3,13 +3,13 @@ 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!(: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!(: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') } diff --git a/spec/features/issues/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb similarity index 52% rename from spec/features/issues/search_bar_spec.rb rename to spec/features/issues/filtered_search/search_bar_spec.rb index d0abdc284eae99..5862214cdc3e3d 100644 --- a/spec/features/issues/search_bar_spec.rb +++ b/spec/features/issues/filtered_search/search_bar_spec.rb @@ -14,6 +14,11 @@ 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' @@ -49,5 +54,33 @@ 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 -- GitLab From d7eadcd6ff8ac471d9aca77d519f624d9a9ccc99 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 16 Dec 2016 01:08:07 -0600 Subject: [PATCH 191/191] 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 364d4bf4db14ed..216cd78850b7f8 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 @@ 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 5862214cdc3e3d..d37057a44f8ce5 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 @@ 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 @@ def getLeftStyle(style) 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 @@ def getLeftStyle(style) 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 @@ def getLeftStyle(style) 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) -- GitLab