From b2592f01da50367f1c6a2acf4ef701d3a7661d36 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 8 Dec 2016 14:39:39 +1100 Subject: Store capybara-screenshots folder as artifacts for RSpec and Spinach --- .gitlab-ci.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e522d47d19d..475346dcd34 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -69,9 +69,11 @@ stages: - knapsack rspec "--color --format documentation" artifacts: expire_in: 31d + when: always paths: - - knapsack/ - coverage/ + - knapsack/ + - tmp/capybara/ .spinach-knapsack: &spinach-knapsack stage: test @@ -87,9 +89,11 @@ stages: - knapsack spinach "-r rerun" || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)' artifacts: expire_in: 31d + when: always paths: - - knapsack/ - coverage/ + - knapsack/ + - tmp/capybara/ # Prepare and merge knapsack tests -- cgit v1.2.1 From a61c19778180a8316321ac9ca6f84de76a6c23b3 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 8 Dec 2016 14:45:34 +1100 Subject: Don't disable capybara-screenshot in CI environment --- features/support/capybara.rb | 9 +++------ spec/support/capybara.rb | 9 +++------ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/features/support/capybara.rb b/features/support/capybara.rb index 47372df152d..0b6a0981a3c 100644 --- a/features/support/capybara.rb +++ b/features/support/capybara.rb @@ -1,5 +1,6 @@ require 'spinach/capybara' require 'capybara/poltergeist' +require 'capybara-screenshot/spinach' # Give CI some extra time timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 15 @@ -20,12 +21,8 @@ end Capybara.default_max_wait_time = timeout Capybara.ignore_hidden_elements = false -unless ENV['CI'] || ENV['CI_SERVER'] - require 'capybara-screenshot/spinach' - - # Keep only the screenshots generated from the last failing test suite - Capybara::Screenshot.prune_strategy = :keep_last_run -end +# Keep only the screenshots generated from the last failing test suite +Capybara::Screenshot.prune_strategy = :keep_last_run Spinach.hooks.before_run do TestEnv.warm_asset_cache unless ENV['CI'] || ENV['CI_SERVER'] diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index 16d5f2bf0b8..ebb1f30f090 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -1,6 +1,7 @@ require 'capybara/rails' require 'capybara/rspec' require 'capybara/poltergeist' +require 'capybara-screenshot/rspec' # Give CI some extra time timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 10 @@ -21,12 +22,8 @@ end Capybara.default_max_wait_time = timeout Capybara.ignore_hidden_elements = true -unless ENV['CI'] || ENV['CI_SERVER'] - require 'capybara-screenshot/rspec' - - # Keep only the screenshots generated from the last failing test suite - Capybara::Screenshot.prune_strategy = :keep_last_run -end +# Keep only the screenshots generated from the last failing test suite +Capybara::Screenshot.prune_strategy = :keep_last_run RSpec.configure do |config| config.before(:suite) do -- cgit v1.2.1 From 7a399b7061d4d374f01ddaa75ae7fba53ca4cb6b Mon Sep 17 00:00:00 2001 From: Matthieu Tardy Date: Mon, 9 Jan 2017 07:38:13 +0100 Subject: Strip reference prefixes on branch creation Signed-off-by: Matthieu Tardy --- ...ranch-names-with-reference-prefixes-results-in-buggy-branches.yml | 4 ++++ lib/gitlab/git_ref_validator.rb | 3 +++ spec/lib/git_ref_validator_spec.rb | 5 +++++ 3 files changed, 12 insertions(+) create mode 100644 changelogs/unreleased/26470-branch-names-with-reference-prefixes-results-in-buggy-branches.yml diff --git a/changelogs/unreleased/26470-branch-names-with-reference-prefixes-results-in-buggy-branches.yml b/changelogs/unreleased/26470-branch-names-with-reference-prefixes-results-in-buggy-branches.yml new file mode 100644 index 00000000000..e82cbf00cfb --- /dev/null +++ b/changelogs/unreleased/26470-branch-names-with-reference-prefixes-results-in-buggy-branches.yml @@ -0,0 +1,4 @@ +--- +title: Strip reference prefixes on branch creation +merge_request: 8498 +author: Matthieu Tardy diff --git a/lib/gitlab/git_ref_validator.rb b/lib/gitlab/git_ref_validator.rb index 4d83d8e72a8..0e87ee30c98 100644 --- a/lib/gitlab/git_ref_validator.rb +++ b/lib/gitlab/git_ref_validator.rb @@ -5,6 +5,9 @@ module Gitlab # # Returns true for a valid reference name, false otherwise def validate(ref_name) + return false if ref_name.start_with?('refs/heads/') + return false if ref_name.start_with?('refs/remotes/') + Gitlab::Utils.system_silent( %W(#{Gitlab.config.git.bin_path} check-ref-format refs/#{ref_name})) end diff --git a/spec/lib/git_ref_validator_spec.rb b/spec/lib/git_ref_validator_spec.rb index dc57e94f193..cc8daa535d6 100644 --- a/spec/lib/git_ref_validator_spec.rb +++ b/spec/lib/git_ref_validator_spec.rb @@ -5,6 +5,7 @@ describe Gitlab::GitRefValidator, lib: true do it { expect(Gitlab::GitRefValidator.validate('implement_@all')).to be_truthy } it { expect(Gitlab::GitRefValidator.validate('my_new_feature')).to be_truthy } it { expect(Gitlab::GitRefValidator.validate('#1')).to be_truthy } + it { expect(Gitlab::GitRefValidator.validate('feature/refs/heads/foo')).to be_truthy } it { expect(Gitlab::GitRefValidator.validate('feature/~new/')).to be_falsey } it { expect(Gitlab::GitRefValidator.validate('feature/^new/')).to be_falsey } it { expect(Gitlab::GitRefValidator.validate('feature/:new/')).to be_falsey } @@ -17,4 +18,8 @@ describe Gitlab::GitRefValidator, lib: true do it { expect(Gitlab::GitRefValidator.validate('feature\new')).to be_falsey } it { expect(Gitlab::GitRefValidator.validate('feature//new')).to be_falsey } it { expect(Gitlab::GitRefValidator.validate('feature new')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('refs/heads/')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('refs/remotes/')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('refs/heads/feature')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('refs/remotes/origin')).to be_falsey } end -- cgit v1.2.1 From 5852e0e0605e90949aec817293f45fabf5b116ac Mon Sep 17 00:00:00 2001 From: Maxime Besson Date: Fri, 24 Feb 2017 12:21:41 +0100 Subject: Suggest a more secure way of handling SSH host keys in docker builds --- doc/ci/ssh_keys/README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md index 49e7ac38b26..688a69d77ba 100644 --- a/doc/ci/ssh_keys/README.md +++ b/doc/ci/ssh_keys/README.md @@ -38,6 +38,15 @@ following **Settings > Variables**. As **Key** add the name `SSH_PRIVATE_KEY` and in the **Value** field paste the content of your _private_ key that you created earlier. +It is also good practice to check the server's own public key to make sure you +are not being targeted by a man-in-the-middle attack. To do this, add another +variable named `SSH_SERVER_HOSTKEYS`. To find out the hostkeys of your server, run +the `ssh-keyscan YOUR_SERVER` command from a trusted network (ideally, from the +server itself), and paste its output into the `SSH_SERVER_HOSTKEY` variable. If +you need to connect to multiple servers, concatenate all the server public keys +that you collected into the **Value** of the variable. There must be one key per +line. + Next you need to modify your `.gitlab-ci.yml` with a `before_script` action. Add it to the top: @@ -59,6 +68,11 @@ before_script: # you will overwrite your user's SSH config. - mkdir -p ~/.ssh - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config' + # In order to properly check the server's host key, assuming you created the + # SSH_SERVER_HOSTKEYS variable previously, uncomment the following two lines + # instead. + # - mkdir -p ~/.ssh + # - '[[ -f /.dockerenv ]] && echo "$SSH_SERVER_HOSTKEYS" > ~/.ssh/known_hosts' ``` As a final step, add the _public_ key from the one you created earlier to the -- cgit v1.2.1 From 7951b8469d81f58132f69ad3a1e71fbd39ef1f49 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Fri, 24 Feb 2017 23:11:50 +0530 Subject: Document U2F limitations with multiple hostnames/FQDNs. --- .../28277-document-u2f-limitations-with-multiple-urls.yml | 4 ++++ doc/user/profile/account/two_factor_authentication.md | 11 +++++++++++ 2 files changed, 15 insertions(+) create mode 100644 changelogs/unreleased/28277-document-u2f-limitations-with-multiple-urls.yml diff --git a/changelogs/unreleased/28277-document-u2f-limitations-with-multiple-urls.yml b/changelogs/unreleased/28277-document-u2f-limitations-with-multiple-urls.yml new file mode 100644 index 00000000000..6e3cd8a60d8 --- /dev/null +++ b/changelogs/unreleased/28277-document-u2f-limitations-with-multiple-urls.yml @@ -0,0 +1,4 @@ +--- +title: Document U2F limitations with multiple URLs +merge_request: 9300 +author: diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md index eaa39a0c4ea..63a3d3c472e 100644 --- a/doc/user/profile/account/two_factor_authentication.md +++ b/doc/user/profile/account/two_factor_authentication.md @@ -215,3 +215,14 @@ you may have cases where authorization always fails because of time differences. [Google Authenticator]: https://support.google.com/accounts/answer/1066447?hl=en [FreeOTP]: https://freeotp.github.io/ [YubiKey]: https://www.yubico.com/products/yubikey-hardware/ + +- The GitLab U2F implementation does _not_ work when the GitLab instance is accessed from +multiple hostnames, or FQDNs. Each U2F registration is linked to the _current hostname_ at +the time of registration, and cannot be used for other hostnames/FQDNs. + + For example, if a user is trying to access a GitLab instance from `first.host.xyz` and `second.host.xyz`: + + - The user logs in via `first.host.xyz` and registers their U2F key. + - The user logs out and attempts to log in via `first.host.xyz` - U2F authentication suceeds. + - The user logs out and attempts to log in via `second.host.xyz` - U2F authentication fails, because + the U2F key has only been registered on `first.host.xyz`. -- cgit v1.2.1 From 850f19c02c53648b16a531a81586c05edcfa7530 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 7 Mar 2017 09:24:01 +0000 Subject: Added filtered search bar to issue boards Closes #28312 --- app/assets/javascripts/boards/boards_bundle.js | 3 +++ .../javascripts/boards/filtered_search_boards.js | 5 +++++ app/assets/javascripts/boards/stores/boards_store.js | 4 ++-- app/assets/stylesheets/framework/filters.scss | 5 +++++ app/views/projects/boards/_show.html.haml | 3 ++- app/views/shared/issuable/_filter.html.haml | 18 ++---------------- app/views/shared/issuable/_search_bar.html.haml | 16 ++++++++++++++-- changelogs/unreleased/issue-boards-new-search-bar.yml | 4 ++++ 8 files changed, 37 insertions(+), 21 deletions(-) create mode 100644 app/assets/javascripts/boards/filtered_search_boards.js create mode 100644 changelogs/unreleased/issue-boards-new-search-bar.yml diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js index 55d13be6e5f..951cb854ce8 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -4,6 +4,7 @@ window.Vue = require('vue'); window.Vue.use(require('vue-resource')); +import FilteredSearchBoards from './filtered_search_boards'; require('./models/issue'); require('./models/label'); require('./models/list'); @@ -26,6 +27,8 @@ $(() => { const Store = gl.issueBoards.BoardsStore; const ModalStore = gl.issueBoards.ModalStore; + new FilteredSearchBoards(); + window.gl = window.gl || {}; if (gl.IssueBoardsApp) { diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js new file mode 100644 index 00000000000..6a00d84faf1 --- /dev/null +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -0,0 +1,5 @@ +export default class FilteredSearchBoards extends gl.FilteredSearchManager { + constructor() { + super('boards'); + } +} diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index 3866c6bbfc6..c902a1d8bfc 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -19,8 +19,8 @@ create () { this.state.lists = []; this.state.filters = { - author_id: gl.utils.getParameterValues('author_id')[0], - assignee_id: gl.utils.getParameterValues('assignee_id')[0], + author_username: gl.utils.getParameterValues('author_username')[0], + assignee_username: gl.utils.getParameterValues('assignee_username')[0], milestone_title: gl.utils.getParameterValues('milestone_title')[0], label_name: gl.utils.getParameterValues('label_name[]'), search: '' diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 8f2150066c7..bf0e8e2b891 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -219,6 +219,11 @@ } } +.filter-dropdown-container { + display: -webkit-flex; + display: flex; +} + .dropdown-menu .filter-dropdown-item { padding: 0; } diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml index 3ae78387938..a3593c9f5db 100644 --- a/app/views/projects/boards/_show.html.haml +++ b/app/views/projects/boards/_show.html.haml @@ -4,6 +4,7 @@ - content_for :page_specific_javascripts do = page_specific_javascript_bundle_tag('common_vue') + = page_specific_javascript_bundle_tag('filtered_search') = page_specific_javascript_bundle_tag('boards') = page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test? @@ -12,7 +13,7 @@ = render "projects/issues/head" -= render 'shared/issuable/filter', type: :boards += render 'shared/issuable/search_bar', type: :boards #board-app.boards-app{ "v-cloak" => true, data: board_data } .boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" } diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index f17ae9f28eb..f0bad69a989 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -1,4 +1,4 @@ -- finder = controller.controller_name == 'issues' || controller.controller_name == 'boards' ? issues_finder : merge_requests_finder +- finder = controller.controller_name == 'issues' ? issues_finder : merge_requests_finder - boards_page = controller.controller_name == 'boards' .issues-filters @@ -34,21 +34,7 @@ %a{ href: page_filter_path(without: issuable_filter_params) } Reset filters .pull-right - - if boards_page - #js-boards-search.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) - #js-add-issues-btn.pull-right.prepend-left-10 - .dropdown.pull-right - %button.btn.btn-create.btn-inverted.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) } } - Add 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: "Add 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 diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 32128f3b3dc..515c3d4258e 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -85,8 +85,20 @@ %span.dropdown-label-box{ style: 'background: {{color}}' } %span.label-title.js-data-value {{title}} - .pull-right.filter-dropdown-container - = render 'shared/sort_dropdown' + .filter-dropdown-container + - if type == :boards + - if can?(current_user, :admin_list, @project) + .dropdown.prepend-left-10 + %button.btn.btn-create.btn-inverted.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) } } + Add 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: "Add list" } + - if can?(current_user, :admin_label, @project) + = render partial: "shared/issuable/label_page_create" + = dropdown_loading + #js-add-issues-btn.prepend-left-10 + - else + = render 'shared/sort_dropdown' - if @bulk_edit .issues_bulk_update.hide diff --git a/changelogs/unreleased/issue-boards-new-search-bar.yml b/changelogs/unreleased/issue-boards-new-search-bar.yml new file mode 100644 index 00000000000..b02be70c470 --- /dev/null +++ b/changelogs/unreleased/issue-boards-new-search-bar.yml @@ -0,0 +1,4 @@ +--- +title: Added new filtered search bar to issue boards +merge_request: +author: -- cgit v1.2.1 From f89782b3f25984794f4f9752979c05d5ed6f0a96 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 7 Mar 2017 11:05:37 +0000 Subject: Changed store Async updates the boards when searching --- app/assets/javascripts/boards/boards_bundle.js | 4 ++-- app/assets/javascripts/boards/components/board.js | 8 ++++---- .../javascripts/boards/filtered_search_boards.js | 9 ++++++++- app/assets/javascripts/boards/models/list.js | 20 ++++++++++++++++---- app/assets/javascripts/boards/stores/boards_store.js | 11 ++++------- .../filtered_search/filtered_search_manager.js | 8 ++++++-- 6 files changed, 40 insertions(+), 20 deletions(-) diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js index 951cb854ce8..6b294290f77 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -27,8 +27,6 @@ $(() => { const Store = gl.issueBoards.BoardsStore; const ModalStore = gl.issueBoards.ModalStore; - new FilteredSearchBoards(); - window.gl = window.gl || {}; if (gl.IssueBoardsApp) { @@ -62,6 +60,8 @@ $(() => { }, created () { gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId); + + new FilteredSearchBoards(Store.filter); }, mounted () { Store.disabled = this.disabled; diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js index 18324de18b3..30d3be453be 100644 --- a/app/assets/javascripts/boards/components/board.js +++ b/app/assets/javascripts/boards/components/board.js @@ -28,16 +28,16 @@ require('./board_list'); data () { return { detailIssue: Store.detail, - filters: Store.state.filters, + filter: Store.filter, }; }, watch: { - filters: { - handler () { + filter: { + handler() { this.list.page = 1; this.list.getIssues(true); }, - deep: true + deep: true, }, detailIssue: { handler () { diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js index 6a00d84faf1..0b11237b03d 100644 --- a/app/assets/javascripts/boards/filtered_search_boards.js +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -1,5 +1,12 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { - constructor() { + constructor(store) { super('boards'); + + this.store = store; + this.destroyOnSubmit = false + } + + updateObject(path) { + this.store.path = path.substr(1); } } diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index f237567208c..ae117aa3900 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -10,7 +10,7 @@ class List { this.title = obj.title; this.type = obj.list_type; this.preset = ['done', 'blank'].indexOf(this.type) > -1; - this.filters = gl.issueBoards.BoardsStore.state.filters; + this.filterPath = gl.issueBoards.BoardsStore.filter.path; this.page = 1; this.loading = true; this.loadingMore = false; @@ -65,12 +65,24 @@ class List { } getIssues (emptyIssues = true) { - const filters = this.filters; const data = { page: this.page }; + gl.issueBoards.BoardsStore.filter.path.split('&').forEach((filterParam) => { + const paramSplit = filterParam.split('='); + const paramKeyNormalized = paramSplit[0].replace('[]', ''); + const isArray = paramSplit[0].indexOf('[]'); + + if (isArray >= 0) { + if (!data[paramKeyNormalized]) { + data[paramKeyNormalized] = []; + } - Object.keys(filters).forEach((key) => { data[key] = filters[key]; }); + data[paramKeyNormalized].push(paramSplit[1]); + } else { + data[paramKeyNormalized] = paramSplit[1]; + } + }); - if (this.label) { + if (this.label && data.label_name) { data.label_name = data.label_name.filter(label => label !== this.label.title); } diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index c902a1d8bfc..d7e3973b327 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -8,6 +8,9 @@ gl.issueBoards.BoardsStore = { disabled: false, + filter: { + path: '', + }, state: {}, detail: { issue: {} @@ -18,13 +21,7 @@ }, create () { this.state.lists = []; - this.state.filters = { - author_username: gl.utils.getParameterValues('author_username')[0], - assignee_username: gl.utils.getParameterValues('assignee_username')[0], - milestone_title: gl.utils.getParameterValues('milestone_title')[0], - label_name: gl.utils.getParameterValues('label_name[]'), - search: '' - }; + this.filter.path = gl.utils.getUrlParamsArray().join('&'); }, addList (listObj) { const list = new List(listObj); diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 58a984048de..56ff091197c 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -106,7 +106,7 @@ if (!activeElements.length) { // Prevent droplab from opening dropdown - this.dropdownManager.destroyDroplab(); + //this.dropdownManager.destroyDroplab(); this.search(); } @@ -345,7 +345,11 @@ const parameterizedUrl = `?scope=all&utf8=✓&${paths.join('&')}`; - gl.utils.visitUrl(parameterizedUrl); + if (this.updateObject) { + this.updateObject(parameterizedUrl); + } else { + gl.utils.visitUrl(parameterizedUrl); + } } getUsernameParams() { -- cgit v1.2.1 From ddf71fcef5d0d7b9952d77d712007008efbb5d3f Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 7 Mar 2017 11:07:26 +0000 Subject: Updates the URL --- app/assets/javascripts/boards/filtered_search_boards.js | 1 + app/assets/javascripts/boards/stores/boards_store.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js index 0b11237b03d..ff8da88e6e8 100644 --- a/app/assets/javascripts/boards/filtered_search_boards.js +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -8,5 +8,6 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { updateObject(path) { this.store.path = path.substr(1); + gl.issueBoards.BoardsStore.updateFiltersUrl(); } } diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index d7e3973b327..28ecb322df7 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -120,7 +120,7 @@ })[0]; }, updateFiltersUrl () { - history.pushState(null, null, `?${$.param(this.state.filters)}`); + history.pushState(null, null, `?${this.filter.path}`); } }; })(); -- cgit v1.2.1 From 107c39a66e621e35f808b3a257789d78bf153894 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 7 Mar 2017 14:28:50 +0000 Subject: Stop droplab from destroying itself is handled async --- app/assets/javascripts/boards/filtered_search_boards.js | 3 ++- .../javascripts/filtered_search/filtered_search_manager.js | 10 ++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js index ff8da88e6e8..43c6d9d7237 100644 --- a/app/assets/javascripts/boards/filtered_search_boards.js +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -3,11 +3,12 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { super('boards'); this.store = store; - this.destroyOnSubmit = false + this.isHandledAsync = true; } updateObject(path) { this.store.path = path.substr(1); + gl.issueBoards.BoardsStore.updateFiltersUrl(); } } diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 56ff091197c..652d6c9be0e 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -105,8 +105,14 @@ e.preventDefault(); if (!activeElements.length) { - // Prevent droplab from opening dropdown - //this.dropdownManager.destroyDroplab(); + if (this.isHandledAsync) { + e.stopImmediatePropagation(); + this.filteredSearchInput.blur(); + this.dropdownManager.resetDropdowns(); + } else { + // Prevent droplab from opening dropdown + this.dropdownManager.destroyDroplab(); + } this.search(); } -- cgit v1.2.1 From ab7bfff08b2ba8d15f1ab5f8fa4449dc53f51bab Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 7 Mar 2017 14:43:17 +0000 Subject: Make changing the URL optional - future proof ourselves for the modal window --- app/assets/javascripts/boards/boards_bundle.js | 7 ++++--- app/assets/javascripts/boards/filtered_search_boards.js | 7 +++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js index 6b294290f77..1731f218f37 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -1,10 +1,11 @@ -/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */ +/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren, no-new */ /* global Vue */ /* global BoardService */ +import FilteredSearchBoards from './filtered_search_boards'; + window.Vue = require('vue'); window.Vue.use(require('vue-resource')); -import FilteredSearchBoards from './filtered_search_boards'; require('./models/issue'); require('./models/label'); require('./models/list'); @@ -61,7 +62,7 @@ $(() => { created () { gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId); - new FilteredSearchBoards(Store.filter); + new FilteredSearchBoards(Store.filter, true); }, mounted () { Store.disabled = this.disabled; diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js index 43c6d9d7237..d00cb123909 100644 --- a/app/assets/javascripts/boards/filtered_search_boards.js +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -1,14 +1,17 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { - constructor(store) { + constructor(store, updateUrl = false) { super('boards'); this.store = store; + this.updateUrl = updateUrl; this.isHandledAsync = true; } updateObject(path) { this.store.path = path.substr(1); - gl.issueBoards.BoardsStore.updateFiltersUrl(); + if (this.updateUrl) { + gl.issueBoards.BoardsStore.updateFiltersUrl(); + } } } -- cgit v1.2.1 From 382fea7b5925ac7dc47ccfd79f7537284e68cd6f Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 7 Mar 2017 15:54:45 +0000 Subject: Handle clear search async --- app/assets/javascripts/filtered_search/filtered_search_manager.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 652d6c9be0e..3478f1130a5 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -107,6 +107,7 @@ if (!activeElements.length) { if (this.isHandledAsync) { e.stopImmediatePropagation(); + this.filteredSearchInput.blur(); this.dropdownManager.resetDropdowns(); } else { @@ -205,6 +206,10 @@ this.handleInputPlaceholder(); this.dropdownManager.resetDropdowns(); + + if (this.isHandledAsync) { + this.search(); + } } handleInputVisualToken() { -- cgit v1.2.1 From 809bba7d02b45938494f8ae471a2b27ce4a40833 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 8 Mar 2017 12:17:01 +0000 Subject: Updated specs --- app/assets/javascripts/boards/boards_bundle.js | 4 +- .../boards/components/issue_card_inner.js | 23 ++-- .../javascripts/boards/filtered_search_boards.js | 14 ++ app/assets/javascripts/boards/models/list.js | 8 +- app/assets/stylesheets/framework/filters.scss | 1 - app/views/shared/issuable/_search_bar.html.haml | 2 +- spec/features/boards/add_issues_modal_spec.rb | 2 +- spec/features/boards/boards_spec.rb | 153 ++++++++------------- spec/features/issuables/default_sort_order_spec.rb | 2 +- 9 files changed, 86 insertions(+), 123 deletions(-) diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js index 1731f218f37..9e9da7dfac4 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -62,7 +62,7 @@ $(() => { created () { gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId); - new FilteredSearchBoards(Store.filter, true); + gl.boardsFilterManager = new FilteredSearchBoards(Store.filter, true); }, mounted () { Store.disabled = this.disabled; @@ -85,7 +85,7 @@ $(() => { }); gl.IssueBoardsSearch = new Vue({ - el: document.getElementById('js-boards-search'), + el: document.getElementById('js-add-list'), data: { filters: Store.state.filters }, diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js index 22a8b971ff8..dce573ed6ca 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.js +++ b/app/assets/javascripts/boards/components/issue_card_inner.js @@ -31,29 +31,22 @@ return !this.list.label || label.id !== this.list.label.id; }, filterByLabel(label, e) { - let labelToggleText = label.title; - const labelIndex = Store.state.filters.label_name.indexOf(label.title); + const filterPath = gl.issueBoards.BoardsStore.filter.path.split('&'); + const labelTitle = encodeURIComponent(label.title); + const param = `label_name[]=${labelTitle}`; + const labelIndex = filterPath.indexOf(param); $(e.currentTarget).tooltip('hide'); if (labelIndex === -1) { - Store.state.filters.label_name.push(label.title); - $('.labels-filter').prepend(``); + filterPath.push(param); } else { - Store.state.filters.label_name.splice(labelIndex, 1); - labelToggleText = Store.state.filters.label_name[0]; - $(`.labels-filter input[name="label_name[]"][value="${label.title}"]`).remove(); + filterPath.splice(labelIndex, 1); } - const selectedLabels = Store.state.filters.label_name; - if (selectedLabels.length === 0) { - labelToggleText = 'Label'; - } else if (selectedLabels.length > 1) { - labelToggleText = `${selectedLabels[0]} + ${selectedLabels.length - 1} more`; - } - - $('.labels-filter .dropdown-toggle-text').text(labelToggleText); + gl.issueBoards.BoardsStore.filter.path = filterPath.join('&'); Store.updateFiltersUrl(); + gl.boardsFilterManager.updateTokens(); }, labelStyle(label) { return { diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js index d00cb123909..3014557c440 100644 --- a/app/assets/javascripts/boards/filtered_search_boards.js +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -14,4 +14,18 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { gl.issueBoards.BoardsStore.updateFiltersUrl(); } } + + updateTokens() { + const tokens = document.querySelectorAll('.js-visual-token'); + + // Remove all the tokens as they will be replaced by the search manager + [].forEach.call(tokens, (el) => { + el.parentNode.removeChild(el); + }); + + this.loadSearchParamsFromURL(); + + // Get the placeholder back if search is empty + this.filteredSearchInput.dispatchEvent(new Event('input')); + } } diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index ae117aa3900..b246c3c1503 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -10,7 +10,6 @@ class List { this.title = obj.title; this.type = obj.list_type; this.preset = ['done', 'blank'].indexOf(this.type) > -1; - this.filterPath = gl.issueBoards.BoardsStore.filter.path; this.page = 1; this.loading = true; this.loadingMore = false; @@ -67,18 +66,20 @@ class List { getIssues (emptyIssues = true) { const data = { page: this.page }; gl.issueBoards.BoardsStore.filter.path.split('&').forEach((filterParam) => { + if (filterParam === '') return; const paramSplit = filterParam.split('='); const paramKeyNormalized = paramSplit[0].replace('[]', ''); const isArray = paramSplit[0].indexOf('[]'); + const value = decodeURIComponent(paramSplit[1]); if (isArray >= 0) { if (!data[paramKeyNormalized]) { data[paramKeyNormalized] = []; } - data[paramKeyNormalized].push(paramSplit[1]); + data[paramKeyNormalized].push(value); } else { - data[paramKeyNormalized] = paramSplit[1]; + data[paramKeyNormalized] = value; } }); @@ -101,6 +102,7 @@ class List { } this.createIssues(data.issues); + console.log(this.issues.length); }); } diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index bf0e8e2b891..dd2daa4b872 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -156,7 +156,6 @@ width: 100%; border: 1px solid $border-color; background-color: $white-light; - max-width: 87%; @media (max-width: $screen-xs-min) { -webkit-flex: 1 1 100%; diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 515c3d4258e..d73556114d8 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -88,7 +88,7 @@ .filter-dropdown-container - if type == :boards - if can?(current_user, :admin_list, @project) - .dropdown.prepend-left-10 + .dropdown.prepend-left-10#js-add-list %button.btn.btn-create.btn-inverted.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) } } Add list .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb index a3e24bb5ffa..f7f2d883d2f 100644 --- a/spec/features/boards/add_issues_modal_spec.rb +++ b/spec/features/boards/add_issues_modal_spec.rb @@ -51,7 +51,7 @@ describe 'Issue Boards add issue modal', :feature, :js do end it 'does not show tooltip on add issues button' do - button = page.find('.issue-boards-search button', text: 'Add issues') + button = page.find('.filter-dropdown-container button', text: 'Add issues') expect(button[:title]).not_to eq("Please add a list to your board first") end diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index ecc356f2505..e11ba10c80c 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -359,17 +359,9 @@ describe 'Issue Boards', feature: true, js: true do context 'filtering' do it 'filters by author' do - page.within '.issues-filters' do - click_button('Author') - wait_for_ajax - - page.within '.dropdown-menu-author' do - click_link(user2.name) - end - wait_for_vue_resource - - expect(find('.js-author-search')).to have_content(user2.name) - end + set_filter("author", user2.username) + click_filter_link(user2.username) + submit_filter wait_for_vue_resource wait_for_board_cards(1, 1) @@ -377,17 +369,9 @@ describe 'Issue Boards', feature: true, js: true do end it 'filters by assignee' do - page.within '.issues-filters' do - click_button('Assignee') - wait_for_ajax - - page.within '.dropdown-menu-assignee' do - click_link(user.name) - end - wait_for_vue_resource - - expect(find('.js-assignee-search')).to have_content(user.name) - end + set_filter("assignee", user.username) + click_filter_link(user.username) + submit_filter wait_for_vue_resource @@ -396,17 +380,9 @@ describe 'Issue Boards', feature: true, js: true do end it 'filters by milestone' do - page.within '.issues-filters' do - click_button('Milestone') - wait_for_ajax - - page.within '.milestone-filter' do - click_link(milestone.title) - end - wait_for_vue_resource - - expect(find('.js-milestone-select')).to have_content(milestone.title) - end + set_filter("milestone", "\"#{milestone.title}\"") + click_filter_link(milestone.title) + submit_filter wait_for_vue_resource wait_for_board_cards(1, 1) @@ -415,16 +391,9 @@ describe 'Issue Boards', feature: true, js: true do end it 'filters by label' do - page.within '.issues-filters' do - click_button('Label') - wait_for_ajax - - page.within '.dropdown-menu-labels' do - click_link(testing.title) - wait_for_vue_resource - find('.dropdown-menu-close').click - end - end + set_filter("label", testing.title) + click_filter_link(testing.title) + submit_filter wait_for_vue_resource wait_for_board_cards(1, 1) @@ -432,19 +401,14 @@ describe 'Issue Boards', feature: true, js: true do end it 'filters by label with space after reload' do - page.within '.issues-filters' do - click_button('Label') - wait_for_ajax - - page.within '.dropdown-menu-labels' do - click_link(accepting.title) - wait_for_vue_resource(spinner: false) - find('.dropdown-menu-close').click - end - end + set_filter("label", "\"#{accepting.title}\"") + click_filter_link(accepting.title) + submit_filter # Test after reload page.evaluate_script 'window.location.reload()' + wait_for_board_cards(1, 1) + wait_for_empty_boards((2..3)) wait_for_vue_resource @@ -460,26 +424,16 @@ describe 'Issue Boards', feature: true, js: true do end it 'removes filtered labels' do - wait_for_vue_resource - - page.within '.labels-filter' do - click_button('Label') - wait_for_ajax - - page.within '.dropdown-menu-labels' do - click_link(testing.title) - wait_for_vue_resource(spinner: false) - end + set_filter("label", testing.title) + click_filter_link(testing.title) + submit_filter - expect(page).to have_css('input[name="label_name[]"]', visible: false) + wait_for_board_cards(1, 1) - page.within '.dropdown-menu-labels' do - click_link(testing.title) - wait_for_vue_resource(spinner: false) - end + find('.clear-search').click + submit_filter - expect(page).not_to have_css('input[name="label_name[]"]', visible: false) - end + wait_for_board_cards(1, 8) end it 'infinite scrolls list with label filter' do @@ -487,16 +441,9 @@ describe 'Issue Boards', feature: true, js: true do create(:labeled_issue, project: project, labels: [planning, testing]) end - page.within '.issues-filters' do - click_button('Label') - wait_for_ajax - - page.within '.dropdown-menu-labels' do - click_link(testing.title) - wait_for_vue_resource - find('.dropdown-menu-close').click - end - end + set_filter("label", testing.title) + click_filter_link(testing.title) + submit_filter wait_for_vue_resource @@ -518,18 +465,13 @@ describe 'Issue Boards', feature: true, js: true do end it 'filters by multiple labels' do - page.within '.issues-filters' do - click_button('Label') - wait_for_ajax + set_filter("label", testing.title) + click_filter_link(testing.title) - page.within(find('.dropdown-menu-labels')) do - click_link(testing.title) - wait_for_vue_resource - click_link(bug.title) - wait_for_vue_resource - find('.dropdown-menu-close').click - end - end + set_filter("label", bug.title) + click_filter_link(bug.title) + + submit_filter wait_for_vue_resource @@ -545,14 +487,14 @@ describe 'Issue Boards', feature: true, js: true do wait_for_vue_resource end + page.within('.tokens-container') do + expect(page).to have_content(bug.title) + end + wait_for_vue_resource wait_for_board_cards(1, 1) wait_for_empty_boards((2..3)) - - page.within('.labels-filter') do - expect(find('.dropdown-toggle-text')).to have_content(bug.title) - end end it 'removes label filter by clicking label button on issue' do @@ -560,16 +502,13 @@ describe 'Issue Boards', feature: true, js: true do page.within(find('.card', match: :first)) do click_button(bug.title) end + wait_for_vue_resource expect(page).to have_selector('.card', count: 1) end wait_for_vue_resource - - page.within('.labels-filter') do - expect(find('.dropdown-toggle-text')).to have_content(bug.title) - end end end end @@ -643,4 +582,20 @@ describe 'Issue Boards', feature: true, js: true do wait_for_board_cards(board, 0) end end + + def set_filter(type, text) + find('.filtered-search').native.send_keys("#{type}:#{text}") + end + + def submit_filter + find('.filtered-search').native.send_keys(:enter) + end + + def click_filter_link(link_text) + page.within('.filtered-search-input-container') do + expect(page).to have_button(link_text) + + click_button(link_text) + end + end end diff --git a/spec/features/issuables/default_sort_order_spec.rb b/spec/features/issuables/default_sort_order_spec.rb index 73553f97d6f..bfe43bff10f 100644 --- a/spec/features/issuables/default_sort_order_spec.rb +++ b/spec/features/issuables/default_sort_order_spec.rb @@ -176,7 +176,7 @@ describe 'Projects > Issuables > Default sort order', feature: true do end def selected_sort_order - find('.pull-right .dropdown button').text.downcase + find('.filter-dropdown-container .dropdown button').text.downcase end def visit_merge_requests_with_state(project, state) -- cgit v1.2.1 From a12b99a7698c851ddb5ea91916e19241fb189ced Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 8 Mar 2017 12:21:07 +0000 Subject: Fixed eslint errors --- app/assets/javascripts/boards/boards_bundle.js | 2 +- app/assets/javascripts/boards/models/list.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js index 9e9da7dfac4..2fd1f43f02c 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -1,4 +1,4 @@ -/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren, no-new */ +/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */ /* global Vue */ /* global BoardService */ diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index b246c3c1503..c2af3bb881c 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -102,7 +102,6 @@ class List { } this.createIssues(data.issues); - console.log(this.issues.length); }); } -- cgit v1.2.1 From 9ef84008d65dcdb5a9e2d83e7a0c053044fc91f7 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 8 Mar 2017 14:00:28 +0000 Subject: Hides on mobile --- app/views/projects/boards/_show.html.haml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml index a3593c9f5db..fa463edd526 100644 --- a/app/views/projects/boards/_show.html.haml +++ b/app/views/projects/boards/_show.html.haml @@ -13,7 +13,8 @@ = render "projects/issues/head" -= render 'shared/issuable/search_bar', type: :boards +.hidden-xs.hidden-sm + = render 'shared/issuable/search_bar', type: :boards #board-app.boards-app{ "v-cloak" => true, data: board_data } .boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" } -- cgit v1.2.1 From d701b39db9d458919976249a4b7c8bb5597b3606 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 8 Mar 2017 14:46:46 +0000 Subject: Fixed up boards filter spec due to CSS classes changing Also fixed issue with Vue resource encoding + in search term --- app/assets/javascripts/boards/models/list.js | 3 ++- spec/features/boards/boards_spec.rb | 14 ++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index c2af3bb881c..ad968d2120f 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -70,7 +70,8 @@ class List { const paramSplit = filterParam.split('='); const paramKeyNormalized = paramSplit[0].replace('[]', ''); const isArray = paramSplit[0].indexOf('[]'); - const value = decodeURIComponent(paramSplit[1]); + let value = decodeURIComponent(paramSplit[1]); + value = value.replace(/\+/g, ' '); if (isArray >= 0) { if (!data[paramKeyNormalized]) { diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index e11ba10c80c..f7e8b78b54d 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -29,7 +29,7 @@ describe 'Issue Boards', feature: true, js: true do end it 'shows tooltip on add issues button' do - button = page.find('.issue-boards-search button', text: 'Add issues') + button = page.find('.filter-dropdown-container button', text: 'Add issues') expect(button[:"data-original-title"]).to eq("Please add a list to your board first") end @@ -115,9 +115,8 @@ describe 'Issue Boards', feature: true, js: true do end it 'search done list' do - page.within('#js-boards-search') do - find('.form-control').set(issue8.title) - end + find('.filtered-search').set(issue8.title) + find('.filtered-search').native.send_keys(:enter) wait_for_vue_resource @@ -127,9 +126,8 @@ describe 'Issue Boards', feature: true, js: true do end it 'search list' do - page.within('#js-boards-search') do - find('.form-control').set(issue5.title) - end + find('.filtered-search').set(issue5.title) + find('.filtered-search').native.send_keys(:enter) wait_for_vue_resource @@ -333,7 +331,7 @@ describe 'Issue Boards', feature: true, js: true do wait_for_vue_resource - expect(find('.issue-boards-search')).to have_selector('.open') + expect(page).to have_css('#js-add-list.open') end it 'creates new list from a new label' do -- cgit v1.2.1 From 236d6595edd2393f4ba4faadd39529fcabe48aec Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 8 Mar 2017 14:53:18 +0000 Subject: Removed previous filter code --- app/assets/javascripts/labels_select.js | 18 ++---------------- app/assets/javascripts/milestone_select.js | 8 +------- app/assets/javascripts/users_select.js | 5 ----- 3 files changed, 3 insertions(+), 28 deletions(-) diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 9e2d14c7f87..c648a0f076c 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -353,31 +353,17 @@ return; } - if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar') && - !$dropdown.closest('.add-issues-modal').length) { - boardsModel = gl.issueBoards.BoardsStore.state.filters; - } else if ($dropdown.closest('.add-issues-modal').length) { + if ($dropdown.closest('.add-issues-modal').length) { boardsModel = gl.issueBoards.ModalStore.store.filter; } if (boardsModel) { if (label.isAny) { boardsModel['label_name'] = []; - } - else if ($el.hasClass('is-active')) { + } else if ($el.hasClass('is-active')) { boardsModel['label_name'].push(label.title); } - else { - var filters = boardsModel['label_name']; - filters = filters.filter(function (filteredLabel) { - return filteredLabel !== label.title; - }); - boardsModel['label_name'] = filters; - } - if (!$dropdown.closest('.add-issues-modal').length) { - gl.issueBoards.BoardsStore.updateFiltersUrl(); - } e.preventDefault(); return; } diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 51fa5c828b3..4c4f94cb9f3 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -124,18 +124,12 @@ return; } - if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar') && - !$dropdown.closest('.add-issues-modal').length) { - boardsStore = gl.issueBoards.BoardsStore.state.filters; - } else if ($dropdown.closest('.add-issues-modal').length) { + if ($dropdown.closest('.add-issues-modal').length) { boardsStore = gl.issueBoards.ModalStore.store.filter; } if (boardsStore) { boardsStore[$dropdown.data('field-name')] = selected.name; - if (!$dropdown.closest('.add-issues-modal').length) { - gl.issueBoards.BoardsStore.updateFiltersUrl(); - } e.preventDefault(); } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { if (selected.name != null) { diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index 27af859f7d8..c7a57b47834 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -217,11 +217,6 @@ } if ($el.closest('.add-issues-modal').length) { gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id; - } else if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) { - selectedId = user.id; - gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = user.id; - gl.issueBoards.BoardsStore.updateFiltersUrl(); - e.preventDefault(); } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { selectedId = user.id; return Issuable.filterResults($dropdown.closest('form')); -- cgit v1.2.1 From 75e78f108f850fe6c70c04a13747d2c40a511774 Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Wed, 8 Mar 2017 16:45:59 +0000 Subject: The GitLab Pages external-http and external-https arguments can be specified multiple times --- config/gitlab.yml.example | 4 ++-- config/initializers/1_settings.rb | 4 ++-- doc/administration/pages/index.md | 27 ++++++++++++++++----------- features/steps/project/pages.rb | 6 +++--- lib/support/init.d/gitlab.default.example | 4 ++-- 5 files changed, 25 insertions(+), 20 deletions(-) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 720df0cac2d..8d0ea603569 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -157,8 +157,8 @@ production: &base host: example.com port: 80 # Set to 443 if you serve the pages with HTTPS https: false # Set to true if you serve the pages with HTTPS - # external_http: "1.1.1.1:80" # If defined, enables custom domain support in GitLab Pages - # external_https: "1.1.1.1:443" # If defined, enables custom domain and certificate support in GitLab Pages + # external_http: ["1.1.1.1:80", "[2001::1]:80"] # If defined, enables custom domain support in GitLab Pages + # external_https: ["1.1.1.1:443", "[2001::1]:443"] # If defined, enables custom domain and certificate support in GitLab Pages ## Mattermost ## For enabling Add to Mattermost button diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index b45d0e23080..e5e90031871 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -278,8 +278,8 @@ Settings.pages['host'] ||= "example.com" Settings.pages['port'] ||= Settings.pages.https ? 443 : 80 Settings.pages['protocol'] ||= Settings.pages.https ? "https" : "http" Settings.pages['url'] ||= Settings.send(:build_pages_url) -Settings.pages['external_http'] ||= false if Settings.pages['external_http'].nil? -Settings.pages['external_https'] ||= false if Settings.pages['external_https'].nil? +Settings.pages['external_http'] ||= false unless Settings.pages['external_http'].present? +Settings.pages['external_https'] ||= false unless Settings.pages['external_https'].present? # # Git LFS diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md index 62b0468da79..0c63b0b59a7 100644 --- a/doc/administration/pages/index.md +++ b/doc/administration/pages/index.md @@ -26,9 +26,9 @@ it works. --- -In the case of [custom domains](#custom-domains) (but not -[wildcard domains](#wildcard-domains)), the Pages daemon needs to listen on -ports `80` and/or `443`. For that reason, there is some flexibility in the way +In the case of [custom domains](#custom-domains) (but not +[wildcard domains](#wildcard-domains)), the Pages daemon needs to listen on +ports `80` and/or `443`. For that reason, there is some flexibility in the way which you can set it up: 1. Run the Pages daemon in the same server as GitLab, listening on a secondary IP. @@ -65,11 +65,13 @@ you need to add a [wildcard DNS A record][wiki-wildcard-dns] pointing to the host that GitLab runs. For example, an entry would look like this: ``` -*.example.io. 1800 IN A 1.1.1.1 +*.example.io. 1800 IN A 1.1.1.1 +*.example.io. 1800 IN AAAA 2001::1 ``` where `example.io` is the domain under which GitLab Pages will be served -and `1.1.1.1` is the IP address of your GitLab instance. +and `1.1.1.1` is the IPv4 address of your GitLab instance and `2001::1` is the +IPv6 address. If you don't have IPv6, you can omit the AAAA record. > **Note:** You should not use the GitLab domain to serve user pages. For more information @@ -141,7 +143,8 @@ outside world. In addition to the wildcard domains, you can also have the option to configure GitLab Pages to work with custom domains. Again, there are two options here: support custom domains with and without TLS certificates. The easiest setup is -that without TLS certificates. +that without TLS certificates. In either case, you'll need a secondary IP. If +you have IPv6 as well as IPv4 addresses, you can use them both. ### Custom domains @@ -163,11 +166,12 @@ world. Custom domains are supported, but no TLS. pages_external_url "http://example.io" nginx['listen_addresses'] = ['1.1.1.1'] pages_nginx['enable'] = false - gitlab_pages['external_http'] = '1.1.1.2:80' + gitlab_pages['external_http'] = ['1.1.1.2:80', '[2001::2]:80'] ``` where `1.1.1.1` is the primary IP address that GitLab is listening to and - `1.1.1.2` the secondary IP where the GitLab Pages daemon listens to. + `1.1.1.2` and `2001::2` are the secondary IPs the GitLab Pages daemon + listens on. If you don't have IPv6, you can omit the IPv6 address. 1. [Reconfigure GitLab][reconfigure] @@ -194,12 +198,13 @@ world. Custom domains and TLS are supported. pages_nginx['enable'] = false gitlab_pages['cert'] = "/etc/gitlab/ssl/example.io.crt" gitlab_pages['cert_key'] = "/etc/gitlab/ssl/example.io.key" - gitlab_pages['external_http'] = '1.1.1.2:80' - gitlab_pages['external_https'] = '1.1.1.2:443' + gitlab_pages['external_http'] = ['1.1.1.2:80', '[2001::2]:80'] + gitlab_pages['external_https'] = ['1.1.1.2:443', '[2001::2]:443'] ``` where `1.1.1.1` is the primary IP address that GitLab is listening to and - `1.1.1.2` the secondary IP where the GitLab Pages daemon listens to. + `1.1.1.2` and `2001::2` are the secondary IPs where the GitLab Pages daemon + listens on. If you don't have IPv6, you can omit the IPv6 address. 1. [Reconfigure GitLab][reconfigure] diff --git a/features/steps/project/pages.rb b/features/steps/project/pages.rb index c80c6273807..4045955a8b9 100644 --- a/features/steps/project/pages.rb +++ b/features/steps/project/pages.rb @@ -53,13 +53,13 @@ class Spinach::Features::ProjectPages < Spinach::FeatureSteps end step 'pages are exposed on external HTTP address' do - allow(Gitlab.config.pages).to receive(:external_http).and_return('1.1.1.1:80') + allow(Gitlab.config.pages).to receive(:external_http).and_return(['1.1.1.1:80']) allow(Gitlab.config.pages).to receive(:external_https).and_return(nil) end step 'pages are exposed on external HTTPS address' do - allow(Gitlab.config.pages).to receive(:external_http).and_return('1.1.1.1:80') - allow(Gitlab.config.pages).to receive(:external_https).and_return('1.1.1.1:443') + allow(Gitlab.config.pages).to receive(:external_http).and_return(['1.1.1.1:80']) + allow(Gitlab.config.pages).to receive(:external_https).and_return(['1.1.1.1:443']) end step 'I should be able to add a New Domain' do diff --git a/lib/support/init.d/gitlab.default.example b/lib/support/init.d/gitlab.default.example index e5797d8fe3c..f6642527639 100644 --- a/lib/support/init.d/gitlab.default.example +++ b/lib/support/init.d/gitlab.default.example @@ -56,14 +56,14 @@ gitlab_workhorse_log="$app_root/log/gitlab-workhorse.log" # The value of -listen-http must be set to `gitlab.yml > pages > external_http` # as well. For example: # -# -listen-http 1.1.1.1:80 +# -listen-http 1.1.1.1:80 -listen-http [2001::1]:80 # # To enable HTTPS support for custom domains add the `-listen-https`, # `-root-cert` and `-root-key` directives in `gitlab_pages_options` below. # The value of -listen-https must be set to `gitlab.yml > pages > external_https` # as well. For example: # -# -listen-https 1.1.1.1:443 -root-cert /path/to/example.com.crt -root-key /path/to/example.com.key +# -listen-https 1.1.1.1:443 -listen-http [2001::1]:443 -root-cert /path/to/example.com.crt -root-key /path/to/example.com.key # # The -pages-domain must be specified the same as in `gitlab.yml > pages > host`. # Set `gitlab_pages_enabled=true` if you want to enable the Pages feature. -- cgit v1.2.1 From 289dd49ef3ec6d99639b8a35d3766f424cb2e022 Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Wed, 8 Mar 2017 12:14:02 -0600 Subject: Fix inconsistent deploy key documentation in UI Deploy keys were added with write access to https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7383 - We still state " Deploy keys allow read-only access to your repository." in the UI. This updates the deploy key UI information to reflect the docs https://docs.gitlab.com/ce/ssh/README.html#deploy-keys --- app/views/projects/deploy_keys/_index.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/deploy_keys/_index.html.haml b/app/views/projects/deploy_keys/_index.html.haml index 0cbe9b3275a..4cfbd9add00 100644 --- a/app/views/projects/deploy_keys/_index.html.haml +++ b/app/views/projects/deploy_keys/_index.html.haml @@ -3,7 +3,7 @@ %h4.prepend-top-0 Deploy Keys %p - Deploy keys allow read-only access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one. + Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one. .col-lg-9 %h5.prepend-top-0 Create a new deploy key for this project -- cgit v1.2.1 From e3b640cab1df6cf7289b01c431d0e3bacc3d127c Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Wed, 8 Mar 2017 15:53:19 -0800 Subject: Add the generated license.csv for 9.0 --- licenses.csv | 945 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 945 insertions(+) create mode 100644 licenses.csv diff --git a/licenses.csv b/licenses.csv new file mode 100644 index 00000000000..a2cbef126ad --- /dev/null +++ b/licenses.csv @@ -0,0 +1,945 @@ +RedCloth,4.3.2,MIT +abbrev,1.0.9,ISC +accepts,1.3.3,MIT +ace-rails-ap,4.1.0,MIT +acorn,4.0.4,MIT +acorn-dynamic-import,2.0.1,MIT +acorn-jsx,3.0.1,MIT +actionmailer,4.2.8,MIT +actionpack,4.2.8,MIT +actionview,4.2.8,MIT +activejob,4.2.8,MIT +activemodel,4.2.8,MIT +activerecord,4.2.8,MIT +activesupport,4.2.8,MIT +acts-as-taggable-on,4.0.0,MIT +addressable,2.3.8,Apache 2.0 +after,0.8.2,MIT +after_commit_queue,1.3.0,MIT +ajv,4.11.2,MIT +ajv-keywords,1.5.1,MIT +akismet,2.0.0,MIT +align-text,0.1.4,MIT +allocations,1.0.5,MIT +amdefine,1.0.1,BSD-3-Clause OR MIT +ansi-escapes,1.4.0,MIT +ansi-html,0.0.7,Apache 2.0 +ansi-regex,2.1.1,MIT +ansi-styles,2.2.1,MIT +anymatch,1.3.0,ISC +append-transform,0.4.0,MIT +aproba,1.1.0,ISC +are-we-there-yet,1.1.2,ISC +arel,6.0.4,MIT +argparse,1.0.9,MIT +arr-diff,2.0.0,MIT +arr-flatten,1.0.1,MIT +array-find,1.0.0,MIT +array-flatten,1.1.1,MIT +array-slice,0.2.3,MIT +array-union,1.0.2,MIT +array-uniq,1.0.3,MIT +array-unique,0.2.1,MIT +arraybuffer.slice,0.0.6,MIT +arrify,1.0.1,MIT +asana,0.4.0,MIT +asciidoctor,1.5.3,MIT +asciidoctor-plantuml,0.0.7,MIT +asn1,0.2.3,MIT +asn1.js,4.9.1,MIT +assert,1.4.1,MIT +assert-plus,0.2.0,MIT +async,0.2.10,MIT +async-each,1.0.1,MIT +asynckit,0.4.0,MIT +attr_encrypted,3.0.3,MIT +attr_required,1.0.0,MIT +autoparse,0.3.3,Apache 2.0 +autoprefixer-rails,6.2.3,MIT +aws-sign2,0.6.0,Apache 2.0 +aws4,1.6.0,MIT +axiom-types,0.1.1,MIT +babel-code-frame,6.22.0,MIT +babel-core,6.23.1,MIT +babel-generator,6.23.0,MIT +babel-helper-bindify-decorators,6.22.0,MIT +babel-helper-builder-binary-assignment-operator-visitor,6.22.0,MIT +babel-helper-call-delegate,6.22.0,MIT +babel-helper-define-map,6.23.0,MIT +babel-helper-explode-assignable-expression,6.22.0,MIT +babel-helper-explode-class,6.22.0,MIT +babel-helper-function-name,6.23.0,MIT +babel-helper-get-function-arity,6.22.0,MIT +babel-helper-hoist-variables,6.22.0,MIT +babel-helper-optimise-call-expression,6.23.0,MIT +babel-helper-regex,6.22.0,MIT +babel-helper-remap-async-to-generator,6.22.0,MIT +babel-helper-replace-supers,6.23.0,MIT +babel-helpers,6.23.0,MIT +babel-loader,6.2.10,MIT +babel-messages,6.23.0,MIT +babel-plugin-check-es2015-constants,6.22.0,MIT +babel-plugin-istanbul,4.0.0,New BSD +babel-plugin-syntax-async-functions,6.13.0,MIT +babel-plugin-syntax-async-generators,6.13.0,MIT +babel-plugin-syntax-class-properties,6.13.0,MIT +babel-plugin-syntax-decorators,6.13.0,MIT +babel-plugin-syntax-dynamic-import,6.18.0,MIT +babel-plugin-syntax-exponentiation-operator,6.13.0,MIT +babel-plugin-syntax-object-rest-spread,6.13.0,MIT +babel-plugin-syntax-trailing-function-commas,6.22.0,MIT +babel-plugin-transform-async-generator-functions,6.22.0,MIT +babel-plugin-transform-async-to-generator,6.22.0,MIT +babel-plugin-transform-class-properties,6.23.0,MIT +babel-plugin-transform-decorators,6.22.0,MIT +babel-plugin-transform-es2015-arrow-functions,6.22.0,MIT +babel-plugin-transform-es2015-block-scoped-functions,6.22.0,MIT +babel-plugin-transform-es2015-block-scoping,6.23.0,MIT +babel-plugin-transform-es2015-classes,6.23.0,MIT +babel-plugin-transform-es2015-computed-properties,6.22.0,MIT +babel-plugin-transform-es2015-destructuring,6.23.0,MIT +babel-plugin-transform-es2015-duplicate-keys,6.22.0,MIT +babel-plugin-transform-es2015-for-of,6.23.0,MIT +babel-plugin-transform-es2015-function-name,6.22.0,MIT +babel-plugin-transform-es2015-literals,6.22.0,MIT +babel-plugin-transform-es2015-modules-amd,6.22.0,MIT +babel-plugin-transform-es2015-modules-commonjs,6.23.0,MIT +babel-plugin-transform-es2015-modules-systemjs,6.23.0,MIT +babel-plugin-transform-es2015-modules-umd,6.23.0,MIT +babel-plugin-transform-es2015-object-super,6.22.0,MIT +babel-plugin-transform-es2015-parameters,6.23.0,MIT +babel-plugin-transform-es2015-shorthand-properties,6.22.0,MIT +babel-plugin-transform-es2015-spread,6.22.0,MIT +babel-plugin-transform-es2015-sticky-regex,6.22.0,MIT +babel-plugin-transform-es2015-template-literals,6.22.0,MIT +babel-plugin-transform-es2015-typeof-symbol,6.23.0,MIT +babel-plugin-transform-es2015-unicode-regex,6.22.0,MIT +babel-plugin-transform-exponentiation-operator,6.22.0,MIT +babel-plugin-transform-object-rest-spread,6.23.0,MIT +babel-plugin-transform-regenerator,6.22.0,MIT +babel-plugin-transform-strict-mode,6.22.0,MIT +babel-preset-es2015,6.22.0,MIT +babel-preset-stage-2,6.22.0,MIT +babel-preset-stage-3,6.22.0,MIT +babel-register,6.23.0,MIT +babel-runtime,6.22.0,MIT +babel-template,6.23.0,MIT +babel-traverse,6.23.1,MIT +babel-types,6.23.0,MIT +babosa,1.0.2,MIT +babylon,6.15.0,MIT +backo2,1.0.2,MIT +balanced-match,0.4.2,MIT +base32,0.3.2,MIT +base64-arraybuffer,0.1.5,MIT +base64-js,1.2.0,MIT +base64id,1.0.0,MIT +batch,0.5.3,MIT +bcrypt,3.1.11,MIT +bcrypt-pbkdf,1.0.1,New BSD +better-assert,1.0.2,MIT +big.js,3.1.3,MIT +binary-extensions,1.8.0,MIT +bindata,2.3.5,ruby +blob,0.0.4,unknown +block-stream,0.0.9,ISC +bluebird,3.4.7,MIT +bn.js,4.11.6,MIT +body-parser,1.16.0,MIT +boom,2.10.1,New BSD +bootstrap-sass,3.3.6,MIT +brace-expansion,1.1.6,MIT +braces,1.8.5,MIT +brorand,1.0.7,MIT +browser,2.2.0,MIT +browserify-aes,1.0.6,MIT +browserify-cipher,1.0.0,MIT +browserify-des,1.0.0,MIT +browserify-rsa,4.0.1,MIT +browserify-sign,4.0.0,ISC +browserify-zlib,0.1.4,MIT +buffer,4.9.1,MIT +buffer-shims,1.0.0,MIT +buffer-xor,1.0.3,MIT +builder,3.2.3,MIT +builtin-modules,1.1.1,MIT +builtin-status-codes,3.0.0,MIT +bytes,2.4.0,MIT +caller-path,0.1.0,MIT +callsite,1.0.0,unknown +callsites,0.2.0,MIT +camelcase,1.2.1,MIT +carrierwave,0.11.2,MIT +caseless,0.11.0,Apache 2.0 +cause,0.1,MIT +center-align,0.1.3,MIT +chalk,1.1.3,MIT +charlock_holmes,0.7.3,MIT +chokidar,1.6.1,MIT +chronic,0.10.2,MIT +chronic_duration,0.10.6,MIT +chunky_png,1.3.5,MIT +cipher-base,1.0.3,MIT +circular-json,0.3.1,MIT +cli-cursor,1.0.2,MIT +cli-width,2.1.0,ISC +cliui,2.1.0,ISC +clone,1.0.2,MIT +co,4.6.0,MIT +code-point-at,1.1.0,MIT +coercible,1.0.0,MIT +coffee-rails,4.1.1,MIT +coffee-script,2.4.1,MIT +coffee-script-source,1.10.0,MIT +colors,1.1.2,MIT +combine-lists,1.0.1,MIT +combined-stream,1.0.5,MIT +commander,2.9.0,MIT +commondir,1.0.1,MIT +component-bind,1.0.0,unknown +component-emitter,1.2.1,MIT +component-inherit,0.0.3,unknown +compressible,2.0.9,MIT +compression,1.6.2,MIT +compression-webpack-plugin,0.3.2,MIT +concat-map,0.0.1,MIT +concat-stream,1.6.0,MIT +concurrent-ruby,1.0.4,MIT +connect,3.5.0,MIT +connect-history-api-fallback,1.3.0,MIT +connection_pool,2.2.1,MIT +console-browserify,1.1.0,MIT +console-control-strings,1.1.0,ISC +constants-browserify,1.0.0,MIT +contains-path,0.1.0,MIT +content-disposition,0.5.2,MIT +content-type,1.0.2,MIT +convert-source-map,1.3.0,MIT +cookie,0.3.1,MIT +cookie-signature,1.0.6,MIT +core-js,2.4.1,MIT +core-util-is,1.0.2,MIT +crack,0.4.3,MIT +create-ecdh,4.0.0,MIT +create-hash,1.1.2,MIT +create-hmac,1.1.4,MIT +creole,0.5.0,ruby +cryptiles,2.0.5,New BSD +crypto-browserify,3.11.0,MIT +css_parser,1.4.1,MIT +custom-event,1.0.1,MIT +d,0.1.1,MIT +d3,3.5.11,New BSD +d3_rails,3.5.11,MIT +dashdash,1.14.1,MIT +date-now,0.1.4,MIT +debug,2.6.0,MIT +decamelize,1.2.0,MIT +deckar01-task_list,1.0.6,MIT +deep-extend,0.4.1,MIT +deep-is,0.1.3,MIT +default-require-extensions,1.0.0,MIT +default_value_for,3.0.2,MIT +defaults,1.0.3,MIT +del,2.2.2,MIT +delayed-stream,1.0.0,MIT +delegates,1.0.0,MIT +depd,1.1.0,MIT +des.js,1.0.0,MIT +descendants_tracker,0.0.4,MIT +destroy,1.0.4,MIT +detect-indent,4.0.0,MIT +devise,4.2.0,MIT +devise-two-factor,3.0.0,MIT +di,0.0.1,MIT +diff-lcs,1.2.5,"MIT,Perl Artistic v2,GNU GPL v2" +diffie-hellman,5.0.2,MIT +diffy,3.1.0,MIT +doctrine,1.5.0,BSD +document-register-element,1.3.0,MIT +dom-serialize,2.2.1,MIT +domain-browser,1.1.7,MIT +domain_name,0.5.20161021,"Simplified BSD,New BSD,Mozilla Public License 2.0" +doorkeeper,4.2.0,MIT +doorkeeper-openid_connect,1.1.2,MIT +dropzone,4.2.0,MIT +dropzonejs-rails,0.7.2,MIT +duplexer,0.1.1,MIT +ecc-jsbn,0.1.1,MIT +ee-first,1.1.1,MIT +ejs,2.5.6,Apache 2.0 +elliptic,6.3.3,MIT +email_reply_trimmer,0.1.6,MIT +emoji-unicode-version,0.2.1,MIT +emojis-list,2.1.0,MIT +encodeurl,1.0.1,MIT +encryptor,3.0.0,MIT +engine.io,1.8.2,MIT +engine.io-client,1.8.2,MIT +engine.io-parser,1.3.2,MIT +enhanced-resolve,3.1.0,MIT +ent,2.2.0,MIT +equalizer,0.0.11,MIT +errno,0.1.4,MIT +error-ex,1.3.0,MIT +erubis,2.7.0,MIT +es5-ext,0.10.12,MIT +es6-iterator,2.0.0,MIT +es6-map,0.1.4,MIT +es6-promise,4.0.5,MIT +es6-set,0.1.4,MIT +es6-symbol,3.1.0,MIT +es6-weak-map,2.0.1,MIT +escape-html,1.0.3,MIT +escape-string-regexp,1.0.5,MIT +escape_utils,1.1.1,MIT +escodegen,1.8.1,Simplified BSD +escope,3.6.0,Simplified BSD +eslint,3.15.0,MIT +eslint-config-airbnb-base,10.0.1,MIT +eslint-import-resolver-node,0.2.3,MIT +eslint-import-resolver-webpack,0.8.1,MIT +eslint-module-utils,2.0.0,MIT +eslint-plugin-filenames,1.1.0,MIT +eslint-plugin-import,2.2.0,MIT +eslint-plugin-jasmine,2.2.0,MIT +espree,3.4.0,Simplified BSD +esprima,3.1.3,Simplified BSD +esrecurse,4.1.0,Simplified BSD +estraverse,4.1.1,Simplified BSD +esutils,2.0.2,BSD +etag,1.7.0,MIT +eve-raphael,0.5.0,Apache 2.0 +event-emitter,0.3.4,MIT +eventemitter3,1.2.0,MIT +events,1.1.1,MIT +eventsource,0.1.6,MIT +evp_bytestokey,1.0.0,MIT +excon,0.52.0,MIT +execjs,2.6.0,MIT +exit-hook,1.1.1,MIT +expand-braces,0.1.2,MIT +expand-brackets,0.1.5,MIT +expand-range,1.8.2,MIT +express,4.14.1,MIT +expression_parser,0.9.0,MIT +extend,3.0.0,MIT +extglob,0.3.2,MIT +extlib,0.9.16,MIT +extract-zip,1.5.0,Simplified BSD +extsprintf,1.0.2,MIT +faraday,0.9.2,MIT +faraday_middleware,0.10.0,MIT +faraday_middleware-multi_json,0.0.6,MIT +fast-levenshtein,2.0.6,MIT +faye-websocket,0.10.0,MIT +fd-slicer,1.0.1,MIT +ffi,1.9.10,BSD +figures,1.7.0,MIT +file-entry-cache,2.0.0,MIT +filename-regex,2.0.0,MIT +fileset,2.0.3,MIT +filesize,3.5.4,New BSD +fill-range,2.2.3,MIT +finalhandler,0.5.1,MIT +find-cache-dir,0.1.1,MIT +find-root,0.1.2,MIT +find-up,2.1.0,MIT +flat-cache,1.2.2,MIT +flowdock,0.7.1,MIT +fog-aws,0.11.0,MIT +fog-core,1.42.0,MIT +fog-google,0.5.0,MIT +fog-json,1.0.2,MIT +fog-local,0.3.0,MIT +fog-openstack,0.1.6,MIT +fog-rackspace,0.1.1,MIT +fog-xml,0.1.2,MIT +font-awesome-rails,4.7.0.1,"MIT,SIL Open Font License" +for-in,0.1.6,MIT +for-own,0.1.4,MIT +forever-agent,0.6.1,Apache 2.0 +form-data,2.1.2,MIT +formatador,0.2.5,MIT +forwarded,0.1.0,MIT +fresh,0.3.0,MIT +fs-extra,1.0.0,MIT +fs.realpath,1.0.0,ISC +fsevents,,unknown +fstream,1.0.10,ISC +fstream-ignore,1.0.5,ISC +function-bind,1.1.0,MIT +gauge,2.7.2,ISC +gemnasium-gitlab-service,0.2.6,MIT +gemojione,3.0.1,MIT +generate-function,2.0.0,MIT +generate-object-property,1.2.0,MIT +get-caller-file,1.0.2,ISC +get_process_mem,0.2.0,MIT +getpass,0.1.6,MIT +gitaly,0.2.1,MIT +github-linguist,4.7.6,MIT +github-markup,1.4.0,MIT +gitlab-flowdock-git-hook,1.0.1,MIT +gitlab-grit,2.8.1,MIT +gitlab-markup,1.5.1,MIT +gitlab_omniauth-ldap,1.2.1,MIT +glob,7.1.1,ISC +glob-base,0.3.0,MIT +glob-parent,2.0.0,ISC +globalid,0.3.7,MIT +globals,9.14.0,MIT +globby,5.0.0,MIT +gollum-grit_adapter,1.0.1,MIT +gollum-lib,4.2.1,MIT +gollum-rugged_adapter,0.4.2,MIT +gon,6.1.0,MIT +google-api-client,0.8.7,Apache 2.0 +google-protobuf,3.2.0,New BSD +googleauth,0.5.1,Apache 2.0 +graceful-fs,4.1.11,ISC +graceful-readlink,1.0.1,MIT +grape,0.19.1,MIT +grape-entity,0.6.0,MIT +grpc,1.1.2,New BSD +gzip-size,3.0.0,MIT +hamlit,2.6.1,MIT +handle-thing,1.2.5,MIT +handlebars,4.0.6,MIT +har-validator,2.0.6,ISC +has,1.0.1,MIT +has-ansi,2.0.0,MIT +has-binary,0.1.7,MIT +has-cors,1.1.0,MIT +has-flag,1.0.0,MIT +has-unicode,2.0.1,ISC +hash.js,1.0.3,MIT +hasha,2.2.0,MIT +hashie,3.5.5,MIT +hawk,3.1.3,New BSD +health_check,2.6.0,MIT +hipchat,1.5.2,MIT +hoek,2.16.3,New BSD +home-or-tmp,2.0.0,MIT +hosted-git-info,2.2.0,ISC +hpack.js,2.1.6,MIT +html-entities,1.2.0,MIT +html-pipeline,1.11.0,MIT +html2text,0.2.0,MIT +htmlentities,4.3.4,MIT +http,0.9.8,MIT +http-cookie,1.0.3,MIT +http-deceiver,1.2.7,MIT +http-errors,1.5.1,MIT +http-form_data,1.0.1,MIT +http-proxy,1.16.2,MIT +http-proxy-middleware,0.17.3,MIT +http-signature,1.1.1,MIT +http_parser.rb,0.6.0,MIT +httparty,0.13.7,MIT +httpclient,2.8.2,ruby +https-browserify,0.0.1,MIT +i18n,0.8.1,MIT +ice_nine,0.11.2,MIT +iconv-lite,0.4.15,MIT +ieee754,1.1.8,New BSD +ignore,3.2.2,MIT +imurmurhash,0.1.4,MIT +indexof,0.0.1,unknown +inflight,1.0.6,ISC +influxdb,0.2.3,MIT +inherits,2.0.3,ISC +ini,1.3.4,ISC +inquirer,0.12.0,MIT +interpret,1.0.1,MIT +invariant,2.2.2,New BSD +invert-kv,1.0.0,MIT +ipaddr.js,1.2.0,MIT +ipaddress,0.8.3,MIT +is-absolute,0.2.6,MIT +is-arrayish,0.2.1,MIT +is-binary-path,1.0.1,MIT +is-buffer,1.1.4,MIT +is-builtin-module,1.0.0,MIT +is-dotfile,1.0.2,MIT +is-equal-shallow,0.1.3,MIT +is-extendable,0.1.1,MIT +is-extglob,1.0.0,MIT +is-finite,1.0.2,MIT +is-fullwidth-code-point,1.0.0,MIT +is-glob,2.0.1,MIT +is-my-json-valid,2.15.0,MIT +is-number,2.1.0,MIT +is-path-cwd,1.0.0,MIT +is-path-in-cwd,1.0.0,MIT +is-path-inside,1.0.0,MIT +is-posix-bracket,0.1.1,MIT +is-primitive,2.0.0,MIT +is-property,1.0.2,MIT +is-relative,0.2.1,MIT +is-resolvable,1.0.0,MIT +is-stream,1.1.0,MIT +is-typedarray,1.0.0,MIT +is-unc-path,0.1.2,MIT +is-utf8,0.2.1,MIT +is-windows,0.2.0,MIT +isarray,1.0.0,MIT +isbinaryfile,3.0.2,MIT +isexe,1.1.2,ISC +isobject,2.1.0,MIT +isstream,0.1.2,MIT +istanbul,0.4.5,New BSD +istanbul-api,1.1.1,New BSD +istanbul-lib-coverage,1.0.1,New BSD +istanbul-lib-hook,1.0.0,New BSD +istanbul-lib-instrument,1.4.2,New BSD +istanbul-lib-report,1.0.0-alpha.3,New BSD +istanbul-lib-source-maps,1.1.0,New BSD +istanbul-reports,1.0.1,New BSD +jasmine-core,2.5.2,MIT +jasmine-jquery,2.1.1,MIT +jira-ruby,1.1.2,MIT +jodid25519,1.0.2,MIT +jquery,2.2.1,MIT +jquery-atwho-rails,1.3.2,MIT +jquery-rails,4.1.1,MIT +jquery-ujs,1.2.1,MIT +js-cookie,2.1.3,MIT +js-tokens,3.0.1,MIT +js-yaml,3.8.1,MIT +jsbn,0.1.0,BSD +jsesc,1.3.0,MIT +json,1.8.6,ruby +json-jwt,1.7.1,MIT +json-loader,0.5.4,MIT +json-schema,0.2.3,"AFLv2.1,BSD" +json-stable-stringify,1.0.1,MIT +json-stringify-safe,5.0.1,ISC +json3,3.3.2,MIT +json5,0.5.1,MIT +jsonfile,2.4.0,MIT +jsonify,0.0.0,Public Domain +jsonpointer,4.0.1,MIT +jsprim,1.3.1,MIT +jwt,1.5.6,MIT +kaminari,0.17.0,MIT +karma,1.4.1,MIT +karma-coverage-istanbul-reporter,0.2.0,MIT +karma-jasmine,1.1.0,MIT +karma-mocha-reporter,2.2.2,MIT +karma-phantomjs-launcher,1.0.2,MIT +karma-sourcemap-loader,0.3.7,MIT +karma-webpack,2.0.2,MIT +kew,0.7.0,Apache 2.0 +kgio,2.10.0,LGPL-2.1+ +kind-of,3.1.0,MIT +klaw,1.3.1,MIT +kubeclient,2.2.0,MIT +launchy,2.4.3,ISC +lazy-cache,1.0.4,MIT +lcid,1.0.0,MIT +levn,0.3.0,MIT +licensee,8.7.0,MIT +little-plugger,1.1.4,MIT +load-json-file,1.1.0,MIT +loader-runner,2.3.0,MIT +loader-utils,0.2.16,MIT +locate-path,2.0.0,MIT +lodash,4.17.4,MIT +lodash._baseget,3.7.2,MIT +lodash._topath,3.8.1,MIT +lodash.camelcase,4.1.1,MIT +lodash.capitalize,4.2.1,MIT +lodash.cond,4.5.2,MIT +lodash.deburr,4.1.0,MIT +lodash.get,3.7.0,MIT +lodash.isarray,3.0.4,MIT +lodash.kebabcase,4.0.1,MIT +lodash.snakecase,4.0.1,MIT +lodash.words,4.2.0,MIT +log4js,0.6.38,Apache 2.0 +logging,2.1.0,MIT +longest,1.0.1,MIT +loofah,2.0.3,MIT +loose-envify,1.3.1,MIT +lru-cache,2.2.4,MIT +mail,2.6.4,MIT +mail_room,0.9.1,MIT +media-typer,0.3.0,MIT +memoist,0.15.0,MIT +memory-fs,0.4.1,MIT +merge-descriptors,1.0.1,MIT +method_source,0.8.2,MIT +methods,1.1.2,MIT +micromatch,2.3.11,MIT +miller-rabin,4.0.0,MIT +mime,1.3.4,MIT +mime-db,1.26.0,MIT +mime-types,2.99.3,"MIT,Artistic-2.0,GPL-2.0" +mimemagic,0.3.0,MIT +mini_portile2,2.1.0,MIT +minimalistic-assert,1.0.0,ISC +minimatch,3.0.3,ISC +minimist,0.0.8,MIT +mkdirp,0.5.1,MIT +moment,2.17.1,MIT +mousetrap,1.4.6,Apache 2.0 +mousetrap-rails,1.4.6,"MIT,Apache" +ms,0.7.2,MIT +multi_json,1.12.1,MIT +multi_xml,0.6.0,MIT +multipart-post,2.0.0,MIT +mustermann,0.4.0,MIT +mustermann-grape,0.4.0,MIT +mute-stream,0.0.5,ISC +nan,2.5.1,MIT +natural-compare,1.4.0,MIT +negotiator,0.6.1,MIT +net-ldap,0.12.1,MIT +net-ssh,3.0.1,MIT +netrc,0.11.0,MIT +node-libs-browser,2.0.0,MIT +node-pre-gyp,0.6.33,New BSD +node-zopfli,2.0.2,MIT +nokogiri,1.6.8.1,MIT +nopt,3.0.6,ISC +normalize-package-data,2.3.5,Simplified BSD +normalize-path,2.0.1,MIT +npmlog,4.0.2,ISC +number-is-nan,1.0.1,MIT +numerizer,0.1.1,MIT +oauth,0.5.1,MIT +oauth-sign,0.8.2,Apache 2.0 +oauth2,1.2.0,MIT +object-assign,4.1.1,MIT +object-component,0.0.3,unknown +object.omit,2.0.1,MIT +obuf,1.1.1,MIT +octokit,4.6.2,MIT +oj,2.17.4,MIT +omniauth,1.4.2,MIT +omniauth-auth0,1.4.1,MIT +omniauth-authentiq,0.3.0,MIT +omniauth-azure-oauth2,0.0.6,MIT +omniauth-cas3,1.1.3,MIT +omniauth-facebook,4.0.0,MIT +omniauth-github,1.1.2,MIT +omniauth-gitlab,1.0.2,MIT +omniauth-google-oauth2,0.4.1,MIT +omniauth-kerberos,0.3.0,MIT +omniauth-multipassword,0.4.2,MIT +omniauth-oauth,1.1.0,MIT +omniauth-oauth2,1.3.1,MIT +omniauth-oauth2-generic,0.2.2,MIT +omniauth-saml,1.7.0,MIT +omniauth-shibboleth,1.2.1,MIT +omniauth-twitter,1.2.1,MIT +omniauth_crowd,2.2.3,MIT +on-finished,2.3.0,MIT +on-headers,1.0.1,MIT +once,1.3.3,ISC +onetime,1.1.0,MIT +opener,1.4.3,(WTFPL OR MIT) +opn,4.0.2,MIT +optimist,0.6.1,MIT/X11 +optionator,0.8.2,MIT +options,0.0.6,MIT +org-ruby,0.9.12,MIT +original,1.0.0,MIT +orm_adapter,0.5.0,MIT +os,0.9.6,MIT +os-browserify,0.2.1,MIT +os-homedir,1.0.2,MIT +os-locale,1.4.0,MIT +os-tmpdir,1.0.2,MIT +p-limit,1.1.0,MIT +p-locate,2.0.0,MIT +pako,0.2.9,MIT +paranoia,2.2.0,MIT +parse-asn1,5.0.0,ISC +parse-glob,3.0.4,MIT +parse-json,2.2.0,MIT +parsejson,0.0.3,MIT +parseqs,0.0.5,MIT +parseuri,0.0.5,MIT +parseurl,1.3.1,MIT +path-browserify,0.0.0,MIT +path-exists,3.0.0,MIT +path-is-absolute,1.0.1,MIT +path-is-inside,1.0.2,(WTFPL OR MIT) +path-parse,1.0.5,MIT +path-to-regexp,0.1.7,MIT +path-type,1.1.0,MIT +pbkdf2,3.0.9,MIT +pend,1.2.0,MIT +pg,0.18.4,"BSD,ruby,GPL" +phantomjs-prebuilt,2.1.14,Apache 2.0 +pify,2.3.0,MIT +pikaday,1.5.1,"BSD,MIT" +pinkie,2.0.4,MIT +pinkie-promise,2.0.1,MIT +pkg-dir,1.0.0,MIT +pkg-up,1.0.0,MIT +pluralize,1.2.1,MIT +portfinder,1.0.13,MIT +posix-spawn,0.3.11,"MIT,LGPL" +prelude-ls,1.1.2,MIT +premailer,1.8.6,New BSD +premailer-rails,1.9.2,MIT +preserve,0.2.0,MIT +private,0.1.7,MIT +process,0.11.9,MIT +process-nextick-args,1.0.7,MIT +progress,1.1.8,MIT +proxy-addr,1.1.3,MIT +prr,0.0.0,MIT +public-encrypt,4.0.0,MIT +punycode,1.4.1,MIT +pyu-ruby-sasl,0.0.3.3,MIT +qjobs,1.1.5,MIT +qs,6.2.0,New BSD +querystring,0.2.0,MIT +querystring-es3,0.2.1,MIT +querystringify,0.0.4,MIT +rack,1.6.5,MIT +rack-accept,0.4.5,MIT +rack-attack,4.4.1,MIT +rack-cors,0.4.0,MIT +rack-oauth2,1.2.3,MIT +rack-protection,1.5.3,MIT +rack-proxy,0.6.0,MIT +rack-test,0.6.3,MIT +rails,4.2.8,MIT +rails-deprecated_sanitizer,1.0.3,MIT +rails-dom-testing,1.0.8,MIT +rails-html-sanitizer,1.0.3,MIT +railties,4.2.8,MIT +rainbow,2.1.0,MIT +raindrops,0.17.0,LGPL-2.1+ +rake,10.5.0,MIT +randomatic,1.1.6,MIT +randombytes,2.0.3,MIT +range-parser,1.2.0,MIT +raphael,2.2.7,MIT +raw-body,2.2.0,MIT +raw-loader,0.5.1,MIT +rc,1.1.6,(BSD-2-Clause OR MIT OR Apache-2.0) +rdoc,4.2.2,ruby +read-pkg,1.1.0,MIT +read-pkg-up,1.0.1,MIT +readable-stream,2.1.5,MIT +readdirp,2.1.0,MIT +readline2,1.0.1,MIT +recaptcha,3.0.0,MIT +rechoir,0.6.2,MIT +recursive-open-struct,1.0.0,MIT +redcarpet,3.4.0,MIT +redis,3.2.2,MIT +redis-actionpack,5.0.1,MIT +redis-activesupport,5.0.1,MIT +redis-namespace,1.5.2,MIT +redis-rack,1.6.0,MIT +redis-rails,5.0.1,MIT +redis-store,1.2.0,MIT +regenerate,1.3.2,MIT +regenerator-runtime,0.10.1,MIT +regenerator-transform,0.9.8,BSD +regex-cache,0.4.3,MIT +regexpu-core,2.0.0,MIT +regjsgen,0.2.0,MIT +regjsparser,0.1.5,BSD +repeat-element,1.1.2,MIT +repeat-string,1.6.1,MIT +repeating,2.0.1,MIT +request,2.79.0,Apache 2.0 +request-progress,2.0.1,MIT +request_store,1.3.1,MIT +require-directory,2.1.1,MIT +require-main-filename,1.0.1,ISC +require-uncached,1.0.3,MIT +requires-port,1.0.0,MIT +resolve,1.2.0,MIT +resolve-from,1.0.1,MIT +responders,2.3.0,MIT +rest-client,2.0.0,MIT +restore-cursor,1.0.1,MIT +retriable,1.4.1,MIT +right-align,0.1.3,MIT +rimraf,2.5.4,ISC +rinku,2.0.0,ISC +ripemd160,1.0.1,New BSD +rotp,2.1.2,MIT +rouge,2.0.7,MIT +rqrcode,0.7.0,MIT +rqrcode-rails3,0.1.7,MIT +ruby-fogbugz,0.2.1,MIT +ruby-prof,0.16.2,Simplified BSD +ruby-saml,1.4.1,MIT +rubyntlm,0.5.2,MIT +rubypants,0.2.0,BSD +rufus-scheduler,3.1.10,MIT +rugged,0.24.0,MIT +run-async,0.1.0,MIT +rx-lite,3.1.2,Apache 2.0 +safe-buffer,5.0.1,MIT +safe_yaml,1.0.4,MIT +sanitize,2.1.0,MIT +sass,3.4.22,MIT +sass-rails,5.0.6,MIT +sawyer,0.8.1,MIT +securecompare,1.0.0,MIT +seed-fu,2.3.6,MIT +select-hose,2.0.0,MIT +select2,3.5.2-browserify,unknown +select2-rails,3.5.9.3,MIT +semver,5.3.0,ISC +send,0.14.2,MIT +sentry-raven,2.0.2,Apache 2.0 +serve-index,1.8.0,MIT +serve-static,1.11.2,MIT +set-blocking,2.0.0,ISC +set-immediate-shim,1.0.1,MIT +setimmediate,1.0.5,MIT +setprototypeof,1.0.2,ISC +settingslogic,2.0.9,MIT +sha.js,2.4.8,MIT +shelljs,0.7.6,New BSD +sidekiq,4.2.7,LGPL +sidekiq-cron,0.4.4,MIT +sidekiq-limit_fetch,3.4.0,MIT +signal-exit,3.0.2,ISC +signet,0.7.3,Apache 2.0 +slack-notifier,1.5.1,MIT +slash,1.0.0,MIT +slice-ansi,0.0.4,MIT +sntp,1.0.9,BSD +socket.io,1.7.2,MIT +socket.io-adapter,0.5.0,MIT +socket.io-client,1.7.2,MIT +socket.io-parser,2.3.1,MIT +sockjs,0.3.18,MIT +sockjs-client,1.1.1,MIT +source-list-map,0.1.8,MIT +source-map,0.5.6,New BSD +source-map-support,0.4.11,MIT +spdx-correct,1.0.2,Apache 2.0 +spdx-expression-parse,1.0.4,(MIT AND CC-BY-3.0) +spdx-license-ids,1.2.2,Unlicense +spdy,3.4.4,MIT +spdy-transport,2.0.18,MIT +sprintf-js,1.0.3,New BSD +sprockets,3.7.1,MIT +sprockets-rails,3.2.0,MIT +sshpk,1.10.2,MIT +state_machines,0.4.0,MIT +state_machines-activemodel,0.4.0,MIT +state_machines-activerecord,0.4.0,MIT +stats-webpack-plugin,0.4.3,MIT +statuses,1.3.1,MIT +stream-browserify,2.0.1,MIT +stream-http,2.6.3,MIT +string-width,1.0.2,MIT +string.fromcodepoint,0.2.1,MIT +string.prototype.codepointat,0.2.0,MIT +string_decoder,0.10.31,MIT +stringex,2.5.2,MIT +stringstream,0.0.5,MIT +strip-ansi,3.0.1,MIT +strip-bom,2.0.0,MIT +strip-json-comments,1.0.4,MIT +supports-color,0.2.0,MIT +sys-filesystem,1.1.6,Artistic 2.0 +table,3.8.3,New BSD +tapable,0.2.6,MIT +tar,2.2.1,ISC +tar-pack,3.3.0,Simplified BSD +temple,0.7.7,MIT +test-exclude,4.0.0,ISC +text-table,0.2.0,MIT +thor,0.19.4,MIT +thread_safe,0.3.6,Apache 2.0 +throttleit,1.0.0,MIT +through,2.3.8,MIT +tilt,2.0.6,MIT +timeago.js,2.0.5,MIT +timers-browserify,2.0.2,MIT +timfel-krb5-auth,0.8.3,LGPL +tmp,0.0.28,MIT +to-array,0.1.4,MIT +to-arraybuffer,1.0.1,MIT +to-fast-properties,1.0.2,MIT +tool,0.2.3,MIT +tough-cookie,2.3.2,New BSD +trim-right,1.0.1,MIT +truncato,0.7.8,MIT +tryit,1.0.3,MIT +tty-browserify,0.0.0,MIT +tunnel-agent,0.4.3,Apache 2.0 +tweetnacl,0.14.5,Unlicense +type-check,0.3.2,MIT +type-is,1.6.14,MIT +typedarray,0.0.6,MIT +tzinfo,1.2.2,MIT +u2f,0.2.1,MIT +uglifier,2.7.2,MIT +uglify-js,2.7.5,Simplified BSD +uglify-to-browserify,1.0.2,MIT +uid-number,0.0.6,ISC +ultron,1.0.2,MIT +unc-path-regex,0.1.2,MIT +underscore,1.8.3,MIT +underscore-rails,1.8.3,MIT +unf,0.1.4,BSD +unf_ext,0.0.7.2,MIT +unicorn,5.1.0,ruby +unicorn-worker-killer,0.4.4,ruby +unpipe,1.0.0,MIT +url,0.11.0,MIT +url-parse,1.0.5,MIT +url_safe_base64,0.2.2,MIT +user-home,2.0.0,MIT +useragent,2.1.12,MIT +util,0.10.3,MIT +util-deprecate,1.0.2,MIT +utils-merge,1.0.0,MIT +uuid,3.0.1,MIT +validate-npm-package-license,3.0.1,Apache 2.0 +validates_hostname,1.0.6,MIT +vary,1.1.0,MIT +verror,1.3.6,MIT +version_sorter,2.1.0,MIT +virtus,1.0.5,MIT +vm-browserify,0.0.4,MIT +vmstat,2.3.0,MIT +void-elements,2.0.1,MIT +vue,2.1.10,MIT +vue-resource,0.9.3,MIT +warden,1.2.6,MIT +watchpack,1.2.1,MIT +wbuf,1.7.2,MIT +webpack,2.2.1,MIT +webpack-bundle-analyzer,2.3.0,MIT +webpack-dev-middleware,1.10.0,MIT +webpack-dev-server,2.3.0,MIT +webpack-rails,0.9.9,MIT +webpack-sources,0.1.4,MIT +websocket-driver,0.6.5,MIT +websocket-extensions,0.1.1,MIT +which,1.2.12,ISC +which-module,1.0.0,ISC +wide-align,1.1.0,ISC +wikicloth,0.8.1,MIT +window-size,0.1.0,MIT +wordwrap,0.0.2,MIT/X11 +wrap-ansi,2.1.0,MIT +wrappy,1.0.2,ISC +write,0.2.1,MIT +ws,1.1.1,MIT +wtf-8,1.0.0,MIT +xmlhttprequest-ssl,1.5.3,MIT +xtend,4.0.1,MIT +y18n,3.2.1,ISC +yargs,3.10.0,MIT +yargs-parser,4.2.1,ISC +yauzl,2.4.1,MIT +yeast,0.1.2,MIT -- cgit v1.2.1 From ac669df526a8cf42cd990b93f5a3f8f87e3eff9d Mon Sep 17 00:00:00 2001 From: mhasbini Date: Thu, 9 Mar 2017 02:14:16 +0200 Subject: link issuable reference to itself in header --- app/helpers/issuables_helper.rb | 19 ++++++++++++++++++- changelogs/unreleased/24137-issuable-permalink.yml | 4 ++++ spec/features/issues/form_spec.rb | 10 ++++++++++ spec/features/merge_requests/form_spec.rb | 11 +++++++++++ 4 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/24137-issuable-permalink.yml diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index c2b399041c6..aad83731b87 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -1,4 +1,6 @@ module IssuablesHelper + include GitlabRoutingHelper + def sidebar_gutter_toggle_icon sidebar_gutter_collapsed? ? icon('angle-double-left', { 'aria-hidden': 'true' }) : icon('angle-double-right', { 'aria-hidden': 'true' }) end @@ -95,8 +97,23 @@ module IssuablesHelper h(milestone_title.presence || default_label) end + def to_url_reference(issuable) + case issuable + when Issue + link_to issuable.to_reference, issue_url(issuable) + when MergeRequest + link_to issuable.to_reference, merge_request_url(issuable) + else + issuable.to_reference + end + end + def issuable_meta(issuable, project, text) - output = content_tag :strong, "#{text} #{issuable.to_reference}", class: "identifier" + output = content_tag(:strong, class: "identifier") do + concat("#{text} ") + concat(to_url_reference(issuable)) + end + output << " opened #{time_ago_with_tooltip(issuable.created_at)} by ".html_safe output << content_tag(:strong) do author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "hidden-xs", tooltip: true) diff --git a/changelogs/unreleased/24137-issuable-permalink.yml b/changelogs/unreleased/24137-issuable-permalink.yml new file mode 100644 index 00000000000..bcc6c6957a1 --- /dev/null +++ b/changelogs/unreleased/24137-issuable-permalink.yml @@ -0,0 +1,4 @@ +--- +title: Link issuable reference to itself in meta-header +merge_request: 9641 +author: mhasbini diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb index d4e0ef91856..755992069ff 100644 --- a/spec/features/issues/form_spec.rb +++ b/spec/features/issues/form_spec.rb @@ -1,6 +1,8 @@ require 'rails_helper' describe 'New/edit issue', feature: true, js: true do + include GitlabRoutingHelper + let!(:project) { create(:project) } let!(:user) { create(:user)} let!(:user2) { create(:user)} @@ -78,6 +80,14 @@ describe 'New/edit issue', feature: true, js: true do expect(page).to have_content label2.title end end + + page.within '.issuable-meta' do + issue = Issue.find_by(title: 'title') + + expect(page).to have_text("Issue #{issue.to_reference}") + # compare paths because the host differ in test + expect(find_link(issue.to_reference)[:href]).to end_with(issue_path(issue)) + end end it 'correctly updates the dropdown toggle when removing a label' do diff --git a/spec/features/merge_requests/form_spec.rb b/spec/features/merge_requests/form_spec.rb index 1ecdb8b5983..f8518f450dc 100644 --- a/spec/features/merge_requests/form_spec.rb +++ b/spec/features/merge_requests/form_spec.rb @@ -1,6 +1,8 @@ require 'rails_helper' describe 'New/edit merge request', feature: true, js: true do + include GitlabRoutingHelper + let!(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } let(:fork_project) { create(:project, forked_from_project: project) } let!(:user) { create(:user)} @@ -84,6 +86,15 @@ describe 'New/edit merge request', feature: true, js: true do expect(page).to have_content label2.title end end + + page.within '.issuable-meta' do + merge_request = MergeRequest.find_by(source_branch: 'fix') + + expect(page).to have_text("Merge Request #{merge_request.to_reference}") + # compare paths because the host differ in test + expect(find_link(merge_request.to_reference)[:href]) + .to end_with(merge_request_path(merge_request)) + end end end -- cgit v1.2.1 From 7d20e47622c9a6e0a780bdbe9b53c8890c00deba Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 20 Feb 2017 12:45:04 +0100 Subject: Add GitLab QA integrations tests to GitLab CE / EE --- qa/.rspec | 3 + qa/Dockerfile | 15 +++++ qa/Gemfile | 8 +++ qa/README.md | 2 + qa/bin/docker | 25 ++++++++ qa/bin/qa | 7 +++ qa/bin/test | 3 + qa/qa.rb | 83 +++++++++++++++++++++++++++ qa/qa/git/repository.rb | 71 +++++++++++++++++++++++ qa/qa/page/admin/license.rb | 20 +++++++ qa/qa/page/admin/menu.rb | 19 ++++++ qa/qa/page/base.rb | 12 ++++ qa/qa/page/main/entry.rb | 26 +++++++++ qa/qa/page/main/groups.rb | 20 +++++++ qa/qa/page/main/menu.rb | 46 +++++++++++++++ qa/qa/page/main/projects.rb | 16 ++++++ qa/qa/page/project/new.rb | 24 ++++++++ qa/qa/page/project/show.rb | 23 ++++++++ qa/qa/runtime/namespace.rb | 15 +++++ qa/qa/runtime/user.rb | 15 +++++ qa/qa/scenario/actable.rb | 23 ++++++++ qa/qa/scenario/gitlab/license/add.rb | 21 +++++++ qa/qa/scenario/gitlab/project/create.rb | 31 ++++++++++ qa/qa/scenario/template.rb | 16 ++++++ qa/qa/scenario/test/instance.rb | 27 +++++++++ qa/qa/specs/config.rb | 78 +++++++++++++++++++++++++ qa/qa/specs/features/login/standard_spec.rb | 14 +++++ qa/qa/specs/features/project/create_spec.rb | 19 ++++++ qa/qa/specs/features/repository/clone_spec.rb | 57 ++++++++++++++++++ qa/qa/specs/features/repository/push_spec.rb | 39 +++++++++++++ qa/qa/specs/runner.rb | 15 +++++ qa/spec/scenario/actable_spec.rb | 47 +++++++++++++++ qa/spec/spec_helper.rb | 19 ++++++ 33 files changed, 859 insertions(+) create mode 100644 qa/.rspec create mode 100644 qa/Dockerfile create mode 100644 qa/Gemfile create mode 100644 qa/README.md create mode 100755 qa/bin/docker create mode 100755 qa/bin/qa create mode 100755 qa/bin/test create mode 100644 qa/qa.rb create mode 100644 qa/qa/git/repository.rb create mode 100644 qa/qa/page/admin/license.rb create mode 100644 qa/qa/page/admin/menu.rb create mode 100644 qa/qa/page/base.rb create mode 100644 qa/qa/page/main/entry.rb create mode 100644 qa/qa/page/main/groups.rb create mode 100644 qa/qa/page/main/menu.rb create mode 100644 qa/qa/page/main/projects.rb create mode 100644 qa/qa/page/project/new.rb create mode 100644 qa/qa/page/project/show.rb create mode 100644 qa/qa/runtime/namespace.rb create mode 100644 qa/qa/runtime/user.rb create mode 100644 qa/qa/scenario/actable.rb create mode 100644 qa/qa/scenario/gitlab/license/add.rb create mode 100644 qa/qa/scenario/gitlab/project/create.rb create mode 100644 qa/qa/scenario/template.rb create mode 100644 qa/qa/scenario/test/instance.rb create mode 100644 qa/qa/specs/config.rb create mode 100644 qa/qa/specs/features/login/standard_spec.rb create mode 100644 qa/qa/specs/features/project/create_spec.rb create mode 100644 qa/qa/specs/features/repository/clone_spec.rb create mode 100644 qa/qa/specs/features/repository/push_spec.rb create mode 100644 qa/qa/specs/runner.rb create mode 100644 qa/spec/scenario/actable_spec.rb create mode 100644 qa/spec/spec_helper.rb diff --git a/qa/.rspec b/qa/.rspec new file mode 100644 index 00000000000..b83d9b7aa65 --- /dev/null +++ b/qa/.rspec @@ -0,0 +1,3 @@ +--color +--format documentation +--require spec_helper diff --git a/qa/Dockerfile b/qa/Dockerfile new file mode 100644 index 00000000000..b4281c02f5a --- /dev/null +++ b/qa/Dockerfile @@ -0,0 +1,15 @@ +FROM ruby:2.3 +LABEL maintainer "Grzegorz Bizon " + +RUN sed -i "s/httpredir.debian.org/ftp.us.debian.org/" /etc/apt/sources.list && \ + apt-get update && apt-get install -y --force-yes \ + libqt5webkit5-dev qt5-qmake qt5-default build-essential xvfb git && \ + apt-get clean + + +WORKDIR /home/qa + +COPY ./ ./ +RUN bundle install + +ENTRYPOINT ["bin/test"] diff --git a/qa/Gemfile b/qa/Gemfile new file mode 100644 index 00000000000..baafc976c4b --- /dev/null +++ b/qa/Gemfile @@ -0,0 +1,8 @@ +source 'https://rubygems.org' + +gem 'capybara', '~> 2.12.1' +gem 'capybara-screenshot', '~> 1.0.14' +gem 'capybara-webkit', '~> 1.12.0' +gem 'rake', '~> 12.0.0' +gem 'rspec', '~> 3.5' +gem 'rubocop', '~> 0.47.1' diff --git a/qa/README.md b/qa/README.md new file mode 100644 index 00000000000..2b4577575c5 --- /dev/null +++ b/qa/README.md @@ -0,0 +1,2 @@ +## Integration tests for GitLab + diff --git a/qa/bin/docker b/qa/bin/docker new file mode 100755 index 00000000000..683e915f698 --- /dev/null +++ b/qa/bin/docker @@ -0,0 +1,25 @@ +#!/bin/sh + +case "$1" in + build) + docker pull $CI_REGISTRY_IMAGE:latest + docker build --cache-from $CI_REGISTRY_IMAGE:latest \ + -t $CI_REGISTRY_IMAGE:ce-latest -t $CI_REGISTRY_IMAGE:ee-latest \ + -t $CI_REGISTRY_IMAGE:ce-nightly -t $CI_REGISTRY_IMAGE:ee-nightly \ + -t $CI_REGISTRY_IMAGE:latest . + ;; + publish) + test -n "$CI_BUILD_TOKEN" || exit 1 + docker login --username gitlab-ci-token --password $CI_BUILD_TOKEN registry.gitlab.com + docker push $CI_REGISTRY_IMAGE:latest + docker push $CI_REGISTRY_IMAGE:ce-latest + docker push $CI_REGISTRY_IMAGE:ee-latest + docker push $CI_REGISTRY_IMAGE:ee-nightly + docker push $CI_REGISTRY_IMAGE:ee-nightly + docker logout registry.gitlab.com + ;; + *) + echo "Usage: $0 [build|publish]" + exit 1 + ;; +esac diff --git a/qa/bin/qa b/qa/bin/qa new file mode 100755 index 00000000000..cecdeac14db --- /dev/null +++ b/qa/bin/qa @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby + +require_relative '../qa' + +QA::Scenario + .const_get(ARGV.shift) + .perform(*ARGV) diff --git a/qa/bin/test b/qa/bin/test new file mode 100755 index 00000000000..997392ad6e4 --- /dev/null +++ b/qa/bin/test @@ -0,0 +1,3 @@ +#!/bin/bash + +xvfb-run bundle exec bin/qa $@ diff --git a/qa/qa.rb b/qa/qa.rb new file mode 100644 index 00000000000..c47561bfa18 --- /dev/null +++ b/qa/qa.rb @@ -0,0 +1,83 @@ +$LOAD_PATH << File.expand_path(File.dirname(__FILE__)) + +module QA + ## + # GitLab QA runtime classes, mostly singletons. + # + module Runtime + autoload :User, 'qa/runtime/user' + autoload :Namespace, 'qa/runtime/namespace' + end + + ## + # GitLab QA Scenarios + # + module Scenario + ## + # Support files + # + autoload :Actable, 'qa/scenario/actable' + autoload :Template, 'qa/scenario/template' + + ## + # Test scenario entrypoints. + # + module Test + autoload :Instance, 'qa/scenario/test/instance' + end + + ## + # GitLab instance scenarios. + # + module Gitlab + module Project + autoload :Create, 'qa/scenario/gitlab/project/create' + end + + module License + autoload :Add, 'qa/scenario/gitlab/license/add' + end + end + end + + ## + # Classes describing structure of GitLab, pages, menus etc. + # + # Needed to execute click-driven-only black-box tests. + # + module Page + autoload :Base, 'qa/page/base' + + module Main + autoload :Entry, 'qa/page/main/entry' + autoload :Menu, 'qa/page/main/menu' + autoload :Groups, 'qa/page/main/groups' + autoload :Projects, 'qa/page/main/projects' + end + + module Project + autoload :New, 'qa/page/project/new' + autoload :Show, 'qa/page/project/show' + end + + module Admin + autoload :Menu, 'qa/page/admin/menu' + autoload :License, 'qa/page/admin/license' + end + end + + ## + # Classes describing operations on Git repositories. + # + module Git + autoload :Repository, 'qa/git/repository' + end + + ## + # Classes that make it possible to execute features tests. + # + module Specs + autoload :Config, 'qa/specs/config' + autoload :Runner, 'qa/specs/runner' + end +end diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb new file mode 100644 index 00000000000..b9e199000d6 --- /dev/null +++ b/qa/qa/git/repository.rb @@ -0,0 +1,71 @@ +require 'uri' + +module QA + module Git + class Repository + include Scenario::Actable + + def self.perform(*args) + Dir.mktmpdir do |dir| + Dir.chdir(dir) { super } + end + end + + def location=(address) + @location = address + @uri = URI(address) + end + + def username=(name) + @username = name + @uri.user = name + end + + def password=(pass) + @password = pass + @uri.password = pass + end + + def use_default_credentials + self.username = Runtime::User.name + self.password = Runtime::User.password + end + + def clone(opts = '') + `git clone #{opts} #{@uri.to_s} ./` + end + + def shallow_clone + clone('--depth 1') + end + + def configure_identity(name, email) + `git config user.name #{name}` + `git config user.email #{email}` + end + + def commit_file(name, contents, message) + add_file(name, contents) + commit(message) + end + + def add_file(name, contents) + File.write(name, contents) + + `git add #{name}` + end + + def commit(message) + `git commit -m "#{message}"` + end + + def push_changes(branch = 'master') + `git push #{@uri.to_s} #{branch}` + end + + def commits + `git log --oneline`.split("\n") + end + end + end +end diff --git a/qa/qa/page/admin/license.rb b/qa/qa/page/admin/license.rb new file mode 100644 index 00000000000..4bdfae30b37 --- /dev/null +++ b/qa/qa/page/admin/license.rb @@ -0,0 +1,20 @@ +module QA + module Page + module Admin + class License < Page::Base + def no_license? + page.has_content?('No GitLab Enterprise Edition ' \ + 'license has been provided yet') + end + + def add_new_license(key) + raise 'License key empty!' if key.to_s.empty? + + choose 'Enter license key' + fill_in 'License key', with: key + click_button 'Upload license' + end + end + end + end +end diff --git a/qa/qa/page/admin/menu.rb b/qa/qa/page/admin/menu.rb new file mode 100644 index 00000000000..b01a4e10f93 --- /dev/null +++ b/qa/qa/page/admin/menu.rb @@ -0,0 +1,19 @@ +module QA + module Page + module Admin + class Menu < Page::Base + def go_to_license + within_middle_menu { click_link 'License' } + end + + private + + def within_middle_menu + page.within('.nav-control') do + yield + end + end + end + end + end +end diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb new file mode 100644 index 00000000000..d55326c5262 --- /dev/null +++ b/qa/qa/page/base.rb @@ -0,0 +1,12 @@ +module QA + module Page + class Base + include Capybara::DSL + include Scenario::Actable + + def refresh + visit current_path + end + end + end +end diff --git a/qa/qa/page/main/entry.rb b/qa/qa/page/main/entry.rb new file mode 100644 index 00000000000..fe80deb6429 --- /dev/null +++ b/qa/qa/page/main/entry.rb @@ -0,0 +1,26 @@ +module QA + module Page + module Main + class Entry < Page::Base + def initialize + visit('/') + + # This resolves cold boot problems with login page + find('.application', wait: 120) + end + + def sign_in_using_credentials + if page.has_content?('Change your password') + fill_in :user_password, with: Runtime::User.password + fill_in :user_password_confirmation, with: Runtime::User.password + click_button 'Change your password' + end + + fill_in :user_login, with: Runtime::User.name + fill_in :user_password, with: Runtime::User.password + click_button 'Sign in' + end + end + end + end +end diff --git a/qa/qa/page/main/groups.rb b/qa/qa/page/main/groups.rb new file mode 100644 index 00000000000..84597719a84 --- /dev/null +++ b/qa/qa/page/main/groups.rb @@ -0,0 +1,20 @@ +module QA + module Page + module Main + class Groups < Page::Base + def prepare_test_namespace + return if page.has_content?(Runtime::Namespace.name) + + click_on 'New Group' + + fill_in 'group_path', with: Runtime::Namespace.name + fill_in 'group_description', + with: "QA test run at #{Runtime::Namespace.time}" + choose 'Private' + + click_button 'Create group' + end + end + end + end +end diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb new file mode 100644 index 00000000000..90ff018b9d2 --- /dev/null +++ b/qa/qa/page/main/menu.rb @@ -0,0 +1,46 @@ +module QA + module Page + module Main + class Menu < Page::Base + def go_to_groups + within_global_menu { click_link 'Groups' } + end + + def go_to_projects + within_global_menu { click_link 'Projects' } + end + + def go_to_admin_area + within_user_menu { click_link 'Admin Area' } + end + + def sign_out + within_user_menu do + find('.header-user-dropdown-toggle').click + click_link('Sign out') + end + end + + def has_personal_area? + page.has_selector?('.header-user-dropdown-toggle') + end + + private + + def within_global_menu + find('.global-dropdown-toggle').click + + page.within('.global-dropdown-menu') do + yield + end + end + + def within_user_menu + page.within('.dropdown-menu-nav') do + yield + end + end + end + end + end +end diff --git a/qa/qa/page/main/projects.rb b/qa/qa/page/main/projects.rb new file mode 100644 index 00000000000..28d3a424022 --- /dev/null +++ b/qa/qa/page/main/projects.rb @@ -0,0 +1,16 @@ +module QA + module Page + module Main + class Projects < Page::Base + def go_to_new_project + ## + # There are 'New Project' and 'New project' buttons on the projects + # page, so we can't use `click_on`. + # + button = find('a', text: /^new project$/i) + button.click + end + end + end + end +end diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb new file mode 100644 index 00000000000..b31bec27b59 --- /dev/null +++ b/qa/qa/page/project/new.rb @@ -0,0 +1,24 @@ +module QA + module Page + module Project + class New < Page::Base + def choose_test_namespace + find('#s2id_project_namespace_id').click + find('.select2-result-label', text: Runtime::Namespace.name).click + end + + def choose_name(name) + fill_in 'project_path', with: name + end + + def add_description(description) + fill_in 'project_description', with: description + end + + def create_new_project + click_on 'Create project' + end + end + end + end +end diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb new file mode 100644 index 00000000000..56a270d8fcc --- /dev/null +++ b/qa/qa/page/project/show.rb @@ -0,0 +1,23 @@ +module QA + module Page + module Project + class Show < Page::Base + def choose_repository_clone_http + find('#clone-dropdown').click + + page.within('#clone-dropdown') do + find('span', text: 'HTTP').click + end + end + + def repository_location + find('#project_clone').value + end + + def wait_for_push + sleep 5 + end + end + end + end +end diff --git a/qa/qa/runtime/namespace.rb b/qa/qa/runtime/namespace.rb new file mode 100644 index 00000000000..e4910b63a14 --- /dev/null +++ b/qa/qa/runtime/namespace.rb @@ -0,0 +1,15 @@ +module QA + module Runtime + module Namespace + extend self + + def time + @time ||= Time.now + end + + def name + 'qa_test_' + time.strftime('%d_%m_%Y_%H-%M-%S') + end + end + end +end diff --git a/qa/qa/runtime/user.rb b/qa/qa/runtime/user.rb new file mode 100644 index 00000000000..12ceda015f0 --- /dev/null +++ b/qa/qa/runtime/user.rb @@ -0,0 +1,15 @@ +module QA + module Runtime + module User + extend self + + def name + ENV['GITLAB_USERNAME'] || 'root' + end + + def password + ENV['GITLAB_PASSWORD'] || 'test1234' + end + end + end +end diff --git a/qa/qa/scenario/actable.rb b/qa/qa/scenario/actable.rb new file mode 100644 index 00000000000..6cdbd24780e --- /dev/null +++ b/qa/qa/scenario/actable.rb @@ -0,0 +1,23 @@ +module QA + module Scenario + module Actable + def act(*args, &block) + instance_exec(*args, &block) + end + + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + def perform + yield new if block_given? + end + + def act(*args, &block) + new.act(*args, &block) + end + end + end + end +end diff --git a/qa/qa/scenario/gitlab/license/add.rb b/qa/qa/scenario/gitlab/license/add.rb new file mode 100644 index 00000000000..ca5e1176959 --- /dev/null +++ b/qa/qa/scenario/gitlab/license/add.rb @@ -0,0 +1,21 @@ +module QA + module Scenario + module Gitlab + module License + class Add < Scenario::Template + def perform + Page::Main::Entry.act { sign_in_using_credentials } + Page::Main::Menu.act { go_to_admin_area } + Page::Admin::Menu.act { go_to_license } + + Page::Admin::License.act do + add_new_license(ENV['EE_LICENSE']) if no_license? + end + + Page::Main::Menu.act { sign_out } + end + end + end + end + end +end diff --git a/qa/qa/scenario/gitlab/project/create.rb b/qa/qa/scenario/gitlab/project/create.rb new file mode 100644 index 00000000000..38522714e64 --- /dev/null +++ b/qa/qa/scenario/gitlab/project/create.rb @@ -0,0 +1,31 @@ +require 'securerandom' + +module QA + module Scenario + module Gitlab + module Project + class Create < Scenario::Template + attr_writer :description + + def name=(name) + @name = "#{name}-#{SecureRandom.hex(8)}" + end + + def perform + Page::Main::Menu.act { go_to_groups } + Page::Main::Groups.act { prepare_test_namespace } + Page::Main::Menu.act { go_to_projects } + Page::Main::Projects.act { go_to_new_project } + + Page::Project::New.perform do |page| + page.choose_test_namespace + page.choose_name(@name) + page.add_description(@description) + page.create_new_project + end + end + end + end + end + end +end diff --git a/qa/qa/scenario/template.rb b/qa/qa/scenario/template.rb new file mode 100644 index 00000000000..341998af160 --- /dev/null +++ b/qa/qa/scenario/template.rb @@ -0,0 +1,16 @@ +module QA + module Scenario + class Template + def self.perform(*args) + new.tap do |scenario| + yield scenario if block_given? + return scenario.perform(*args) + end + end + + def perform(*_args) + raise NotImplementedError + end + end + end +end diff --git a/qa/qa/scenario/test/instance.rb b/qa/qa/scenario/test/instance.rb new file mode 100644 index 00000000000..dcd0a32d79d --- /dev/null +++ b/qa/qa/scenario/test/instance.rb @@ -0,0 +1,27 @@ +module QA + module Scenario + module Test + ## + # Run test suite against any GitLab instance, + # including staging and on-premises installation. + # + class Instance < Scenario::Template + def perform(address, tag, *files) + Specs::Config.perform do |specs| + specs.address = address + end + + ## + # Temporary CE + EE support + Scenario::Gitlab::License::Add.perform if tag.to_s == 'ee' + + Specs::Runner.perform do |specs| + files = files.any? ? files : 'qa/specs/features' + + specs.rspec('--tty', '--tag', tag.to_s, files) + end + end + end + end + end +end diff --git a/qa/qa/specs/config.rb b/qa/qa/specs/config.rb new file mode 100644 index 00000000000..d72187fcd34 --- /dev/null +++ b/qa/qa/specs/config.rb @@ -0,0 +1,78 @@ +require 'rspec/core' +require 'capybara/rspec' +require 'capybara-webkit' +require 'capybara-screenshot/rspec' + +# rubocop:disable Metrics/MethodLength +# rubocop:disable Metrics/LineLength + +module QA + module Specs + class Config < Scenario::Template + attr_writer :address + + def initialize + @address = ENV['GITLAB_URL'] + end + + def perform + raise 'Please configure GitLab address!' unless @address + + configure_rspec! + configure_capybara! + configure_webkit! + end + + def configure_rspec! + RSpec.configure do |config| + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`. + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # Run specs in random order to surface order dependencies. + config.order = :random + Kernel.srand config.seed + + config.before(:all) do + page.current_window.resize_to(1200, 1800) + end + + config.formatter = :documentation + config.color = true + end + end + + def configure_capybara! + Capybara.configure do |config| + config.app_host = @address + config.default_driver = :webkit + config.javascript_driver = :webkit + config.default_max_wait_time = 4 + + # https://github.com/mattheworiordan/capybara-screenshot/issues/164 + config.save_path = 'tmp' + end + end + + def configure_webkit! + Capybara::Webkit.configure do |config| + config.allow_url(@address) + config.block_unknown_urls + end + rescue RuntimeError # rubocop:disable Lint/HandleExceptions + # TODO, Webkit is already configured, this make this + # configuration step idempotent, should be improved. + end + end + end +end diff --git a/qa/qa/specs/features/login/standard_spec.rb b/qa/qa/specs/features/login/standard_spec.rb new file mode 100644 index 00000000000..ecb3f0cb68c --- /dev/null +++ b/qa/qa/specs/features/login/standard_spec.rb @@ -0,0 +1,14 @@ +module QA + feature 'standard root login', :ce, :ee do + scenario 'user logs in using credentials' do + Page::Main::Entry.act { sign_in_using_credentials } + + # TODO, since `Signed in successfully` message was removed + # this is the only way to tell if user is signed in correctly. + # + Page::Main::Menu.perform do |menu| + expect(menu).to have_personal_area + end + end + end +end diff --git a/qa/qa/specs/features/project/create_spec.rb b/qa/qa/specs/features/project/create_spec.rb new file mode 100644 index 00000000000..cf4226252a6 --- /dev/null +++ b/qa/qa/specs/features/project/create_spec.rb @@ -0,0 +1,19 @@ +module QA + feature 'create a new project', :ce, :ee, :staging do + scenario 'user creates a new project' do + Page::Main::Entry.act { sign_in_using_credentials } + + Scenario::Gitlab::Project::Create.perform do |project| + project.name = 'awesome-project' + project.description = 'create awesome project test' + end + + expect(page).to have_content( + /Project \S?awesome-project\S+ was successfully created/ + ) + + expect(page).to have_content('create awesome project test') + expect(page).to have_content('The repository for this project is empty') + end + end +end diff --git a/qa/qa/specs/features/repository/clone_spec.rb b/qa/qa/specs/features/repository/clone_spec.rb new file mode 100644 index 00000000000..a772dc227e3 --- /dev/null +++ b/qa/qa/specs/features/repository/clone_spec.rb @@ -0,0 +1,57 @@ +module QA + feature 'clone code from the repository', :ce, :ee, :staging do + context 'with regular account over http' do + given(:location) do + Page::Project::Show.act do + choose_repository_clone_http + repository_location + end + end + + before do + Page::Main::Entry.act { sign_in_using_credentials } + + Scenario::Gitlab::Project::Create.perform do |scenario| + scenario.name = 'project-with-code' + scenario.description = 'project for git clone tests' + end + + Git::Repository.perform do |repository| + repository.location = location + repository.use_default_credentials + + repository.act do + clone + configure_identity('GitLab QA', 'root@gitlab.com') + commit_file('test.rb', 'class Test; end', 'Add Test class') + commit_file('README.md', '# Test', 'Add Readme') + push_changes + end + end + end + + scenario 'user performs a deep clone' do + Git::Repository.perform do |repository| + repository.location = location + repository.use_default_credentials + + repository.act { clone } + + expect(repository.commits.size).to eq 2 + end + end + + scenario 'user performs a shallow clone' do + Git::Repository.perform do |repository| + repository.location = location + repository.use_default_credentials + + repository.act { shallow_clone } + + expect(repository.commits.size).to eq 1 + expect(repository.commits.first).to include 'Add Readme' + end + end + end + end +end diff --git a/qa/qa/specs/features/repository/push_spec.rb b/qa/qa/specs/features/repository/push_spec.rb new file mode 100644 index 00000000000..4b6cb7908bb --- /dev/null +++ b/qa/qa/specs/features/repository/push_spec.rb @@ -0,0 +1,39 @@ +module QA + feature 'push code to repository', :ce, :ee, :staging do + context 'with regular account over http' do + scenario 'user pushes code to the repository' do + Page::Main::Entry.act { sign_in_using_credentials } + + Scenario::Gitlab::Project::Create.perform do |scenario| + scenario.name = 'project_with_code' + scenario.description = 'project with repository' + end + + Git::Repository.perform do |repository| + repository.location = Page::Project::Show.act do + choose_repository_clone_http + repository_location + end + + repository.use_default_credentials + + repository.act do + clone + configure_identity('GitLab QA', 'root@gitlab.com') + add_file('README.md', '# This is test project') + commit('Add README.md') + push_changes + end + end + + Page::Project::Show.act do + wait_for_push + refresh + end + + expect(page).to have_content('README.md') + expect(page).to have_content('This is test project') + end + end + end +end diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb new file mode 100644 index 00000000000..83ae15d0995 --- /dev/null +++ b/qa/qa/specs/runner.rb @@ -0,0 +1,15 @@ +require 'rspec/core' + +module QA + module Specs + class Runner + include Scenario::Actable + + def rspec(*args) + RSpec::Core::Runner.run(args.flatten, $stderr, $stdout).tap do |status| + abort if status.nonzero? + end + end + end + end +end diff --git a/qa/spec/scenario/actable_spec.rb b/qa/spec/scenario/actable_spec.rb new file mode 100644 index 00000000000..422763910e4 --- /dev/null +++ b/qa/spec/scenario/actable_spec.rb @@ -0,0 +1,47 @@ +describe QA::Scenario::Actable do + subject do + Class.new do + include QA::Scenario::Actable + + attr_accessor :something + + def do_something(arg = nil) + "some#{arg}" + end + end + end + + describe '.act' do + it 'provides means to run steps' do + result = subject.act { do_something } + + expect(result).to eq 'some' + end + + it 'supports passing variables' do + result = subject.act('thing') do |variable| + do_something(variable) + end + + expect(result).to eq 'something' + end + + it 'returns value from the last method' do + result = subject.act { 'test' } + + expect(result).to eq 'test' + end + end + + describe '.perform' do + it 'makes it possible to pass binding' do + variable = 'something' + + result = subject.perform do |object| + object.something = variable + end + + expect(result).to eq 'something' + end + end +end diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb new file mode 100644 index 00000000000..c07a3234673 --- /dev/null +++ b/qa/spec/spec_helper.rb @@ -0,0 +1,19 @@ +require_relative '../qa' + +RSpec.configure do |config| + config.expect_with :rspec do |expectations| + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + config.mock_with :rspec do |mocks| + mocks.verify_partial_doubles = true + end + + config.shared_context_metadata_behavior = :apply_to_host_groups + config.disable_monkey_patching! + config.expose_dsl_globally = true + config.warnings = true + config.profile_examples = 10 + config.order = :random + Kernel.srand config.seed +end -- cgit v1.2.1 From 50cd3990c320464a8d72d522599774111f59913f Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 20 Feb 2017 13:05:27 +0100 Subject: Remove legacy scripts for building docker images [ci skip] --- qa/bin/docker | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100755 qa/bin/docker diff --git a/qa/bin/docker b/qa/bin/docker deleted file mode 100755 index 683e915f698..00000000000 --- a/qa/bin/docker +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/sh - -case "$1" in - build) - docker pull $CI_REGISTRY_IMAGE:latest - docker build --cache-from $CI_REGISTRY_IMAGE:latest \ - -t $CI_REGISTRY_IMAGE:ce-latest -t $CI_REGISTRY_IMAGE:ee-latest \ - -t $CI_REGISTRY_IMAGE:ce-nightly -t $CI_REGISTRY_IMAGE:ee-nightly \ - -t $CI_REGISTRY_IMAGE:latest . - ;; - publish) - test -n "$CI_BUILD_TOKEN" || exit 1 - docker login --username gitlab-ci-token --password $CI_BUILD_TOKEN registry.gitlab.com - docker push $CI_REGISTRY_IMAGE:latest - docker push $CI_REGISTRY_IMAGE:ce-latest - docker push $CI_REGISTRY_IMAGE:ee-latest - docker push $CI_REGISTRY_IMAGE:ee-nightly - docker push $CI_REGISTRY_IMAGE:ee-nightly - docker logout registry.gitlab.com - ;; - *) - echo "Usage: $0 [build|publish]" - exit 1 - ;; -esac -- cgit v1.2.1 From e63f3e849fd7ce14b6714b0021b2a17b3179d916 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 20 Feb 2017 13:18:41 +0100 Subject: Extend README for GitLab QA files in `qa/` [ci skip] --- qa/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qa/README.md b/qa/README.md index 2b4577575c5..841e787488c 100644 --- a/qa/README.md +++ b/qa/README.md @@ -1,2 +1,5 @@ ## Integration tests for GitLab +This directory contains integration tests for GitLab. + +It is part of [GitLab QA project](https://gitlab.com/gitlab-org/gitlab-qa). -- cgit v1.2.1 From adbbcc1cf43cdbea51db23338b14670da444b2b8 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 20 Feb 2017 13:19:47 +0100 Subject: Remove blank line from GitLab QA Dockerfile --- qa/Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/qa/Dockerfile b/qa/Dockerfile index b4281c02f5a..2814a7bdef0 100644 --- a/qa/Dockerfile +++ b/qa/Dockerfile @@ -6,7 +6,6 @@ RUN sed -i "s/httpredir.debian.org/ftp.us.debian.org/" /etc/apt/sources.list && libqt5webkit5-dev qt5-qmake qt5-default build-essential xvfb git && \ apt-get clean - WORKDIR /home/qa COPY ./ ./ -- cgit v1.2.1 From 161d0aa43dc767485c6f8a2300b6f4014c29ad7b Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 20 Feb 2017 13:59:11 +0100 Subject: Fix Rubocop offense and remove QA Rubocop from deps --- qa/Gemfile | 1 - qa/qa.rb | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/qa/Gemfile b/qa/Gemfile index baafc976c4b..6bfe25ba437 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -5,4 +5,3 @@ gem 'capybara-screenshot', '~> 1.0.14' gem 'capybara-webkit', '~> 1.12.0' gem 'rake', '~> 12.0.0' gem 'rspec', '~> 3.5' -gem 'rubocop', '~> 0.47.1' diff --git a/qa/qa.rb b/qa/qa.rb index c47561bfa18..106761fd215 100644 --- a/qa/qa.rb +++ b/qa/qa.rb @@ -1,4 +1,4 @@ -$LOAD_PATH << File.expand_path(File.dirname(__FILE__)) +$: << File.expand_path(File.dirname(__FILE__)) module QA ## -- cgit v1.2.1 From 37d68f217b6565e2e3991c14cc46c60de8a71632 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 9 Mar 2017 10:41:10 +0100 Subject: Extend README.md for GitLab QA in `qa/` directory [ci skip] --- qa/README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/qa/README.md b/qa/README.md index 841e787488c..b6b5a76f1d3 100644 --- a/qa/README.md +++ b/qa/README.md @@ -3,3 +3,16 @@ This directory contains integration tests for GitLab. It is part of [GitLab QA project](https://gitlab.com/gitlab-org/gitlab-qa). + +## What GitLab QA is? + +GitLab QA is an integration tests suite for GitLab. + +These are black-box and entirely click-driven integration tests you can run +against any existing instance. + +## How does it work? + +1. When we release a new version of GitLab, we build a Docker images for it. +1. Along with GitLab Docker Images we also build and publish GitLab QA images. +1. GitLab QA project uses these images to execute integration tests. -- cgit v1.2.1 From 2ae578c6b11bcd06b0c6dc6827e01de09f2747cc Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 9 Mar 2017 10:48:18 +0100 Subject: Add env var that describes QA release to Dockerfile --- qa/Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qa/Dockerfile b/qa/Dockerfile index 2814a7bdef0..2a1390193e7 100644 --- a/qa/Dockerfile +++ b/qa/Dockerfile @@ -1,6 +1,8 @@ FROM ruby:2.3 LABEL maintainer "Grzegorz Bizon " +ENV GITLAB_RELEASE CE + RUN sed -i "s/httpredir.debian.org/ftp.us.debian.org/" /etc/apt/sources.list && \ apt-get update && apt-get install -y --force-yes \ libqt5webkit5-dev qt5-qmake qt5-default build-essential xvfb git && \ -- cgit v1.2.1 From 5becdf01941e3a471def26dd82282784c58b5590 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 9 Mar 2017 12:18:55 +0100 Subject: Implement GitLab QA release inflection strategy --- qa/qa.rb | 3 ++ qa/qa/runtime/release.rb | 45 ++++++++++++++++++++++++++++++ qa/spec/runtime/release_spec.rb | 62 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 qa/qa/runtime/release.rb create mode 100644 qa/spec/runtime/release_spec.rb diff --git a/qa/qa.rb b/qa/qa.rb index 106761fd215..3cc542b5c16 100644 --- a/qa/qa.rb +++ b/qa/qa.rb @@ -5,6 +5,7 @@ module QA # GitLab QA runtime classes, mostly singletons. # module Runtime + autoload :Release, 'qa/runtime/release' autoload :User, 'qa/runtime/user' autoload :Namespace, 'qa/runtime/namespace' end @@ -81,3 +82,5 @@ module QA autoload :Runner, 'qa/specs/runner' end end + +QA::Runtime::Release.autoloads diff --git a/qa/qa/runtime/release.rb b/qa/qa/runtime/release.rb new file mode 100644 index 00000000000..d64b478a41a --- /dev/null +++ b/qa/qa/runtime/release.rb @@ -0,0 +1,45 @@ +module QA + module Runtime + ## + # Class that is responsible for plugging CE/EE extensions in, depending on + # environment variable GITLAB_RELEASE that should be present in the runtime + # environment. + # + # We need that to reduce the probability of conflicts when merging + # CE to EE. + # + class Release + UnspecifiedReleaseError = Class.new(StandardError) + + def initialize(version = ENV['GITLAB_RELEASE']) + @version = version.to_s.upcase + + unless %w[CE EE].include?(@version) + raise UnspecifiedReleaseError, 'GITLAB_RELEASE env not defined!' + end + + begin + require "#{version.downcase}/strategy" + rescue LoadError + # noop + end + end + + def has_strategy? + QA.const_defined?("#{@version}::Strategy") + end + + def strategy + QA.const_get("#{@version}::Strategy") + end + + def self.method_missing(name, *args) + @release ||= self.new + + if @release.has_strategy? + @release.strategy.public_send(name, *args) + end + end + end + end +end diff --git a/qa/spec/runtime/release_spec.rb b/qa/spec/runtime/release_spec.rb new file mode 100644 index 00000000000..4995ad48ee6 --- /dev/null +++ b/qa/spec/runtime/release_spec.rb @@ -0,0 +1,62 @@ +describe QA::Runtime::Release do + context 'when release version has extension strategy' do + subject { described_class.new('CE') } + let(:strategy) { spy('CE::Strategy') } + + before do + stub_const('QA::CE::Strategy', strategy) + end + + describe '#has_strategy?' do + it 'return true' do + expect(subject.has_strategy?).to be true + end + end + + describe '#strategy' do + it 'return the strategy constant' do + expect(subject.strategy).to eq QA::CE::Strategy + end + end + + describe 'delegated class methods' do + it 'delegates all calls to strategy class' do + described_class.some_method(1, 2) + + expect(strategy).to have_received(:some_method) + .with(1, 2) + end + end + end + + context 'when release version does not have extension strategy' do + subject { described_class.new('CE') } + + describe '#has_strategy?' do + it 'returns false' do + expect(subject.has_strategy?).to be false + end + end + + describe '#strategy' do + it 'raises error' do + expect { subject.strategy }.to raise_error(NameError) + end + end + + describe 'delegated class methods' do + it 'behaves like a null object and does nothing' do + expect { described_class.some_method(2, 3) }.not_to raise_error + end + end + end + + context 'when release version is invalid or unspecified' do + describe '#new' do + it 'raises an exception' do + expect { described_class.new(nil) } + .to raise_error(described_class::UnspecifiedReleaseError) + end + end + end +end -- cgit v1.2.1 From 92c3a9941cb519ed7ef18d09338bf4b855e3b911 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 9 Mar 2017 12:22:47 +0100 Subject: Fix using release inflector to define autoloads --- qa/qa.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qa/qa.rb b/qa/qa.rb index 3cc542b5c16..bc54f20e17b 100644 --- a/qa/qa.rb +++ b/qa/qa.rb @@ -83,4 +83,6 @@ module QA end end -QA::Runtime::Release.autoloads +if QA::Runtime::Release.has_autoloads? + require QA::Runtime::Release.autoloads_file +end -- cgit v1.2.1 From 6373ef07c74b91b489d58cd9e20f7e5ea4c47664 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 9 Mar 2017 12:29:55 +0100 Subject: Remove EE classes from GitLab QA merged into CE --- qa/qa.rb | 5 ----- qa/qa/page/admin/license.rb | 20 -------------------- qa/qa/scenario/gitlab/license/add.rb | 21 --------------------- qa/qa/scenario/test/instance.rb | 5 +++-- 4 files changed, 3 insertions(+), 48 deletions(-) delete mode 100644 qa/qa/page/admin/license.rb delete mode 100644 qa/qa/scenario/gitlab/license/add.rb diff --git a/qa/qa.rb b/qa/qa.rb index bc54f20e17b..7fe18676634 100644 --- a/qa/qa.rb +++ b/qa/qa.rb @@ -34,10 +34,6 @@ module QA module Project autoload :Create, 'qa/scenario/gitlab/project/create' end - - module License - autoload :Add, 'qa/scenario/gitlab/license/add' - end end end @@ -63,7 +59,6 @@ module QA module Admin autoload :Menu, 'qa/page/admin/menu' - autoload :License, 'qa/page/admin/license' end end diff --git a/qa/qa/page/admin/license.rb b/qa/qa/page/admin/license.rb deleted file mode 100644 index 4bdfae30b37..00000000000 --- a/qa/qa/page/admin/license.rb +++ /dev/null @@ -1,20 +0,0 @@ -module QA - module Page - module Admin - class License < Page::Base - def no_license? - page.has_content?('No GitLab Enterprise Edition ' \ - 'license has been provided yet') - end - - def add_new_license(key) - raise 'License key empty!' if key.to_s.empty? - - choose 'Enter license key' - fill_in 'License key', with: key - click_button 'Upload license' - end - end - end - end -end diff --git a/qa/qa/scenario/gitlab/license/add.rb b/qa/qa/scenario/gitlab/license/add.rb deleted file mode 100644 index ca5e1176959..00000000000 --- a/qa/qa/scenario/gitlab/license/add.rb +++ /dev/null @@ -1,21 +0,0 @@ -module QA - module Scenario - module Gitlab - module License - class Add < Scenario::Template - def perform - Page::Main::Entry.act { sign_in_using_credentials } - Page::Main::Menu.act { go_to_admin_area } - Page::Admin::Menu.act { go_to_license } - - Page::Admin::License.act do - add_new_license(ENV['EE_LICENSE']) if no_license? - end - - Page::Main::Menu.act { sign_out } - end - end - end - end - end -end diff --git a/qa/qa/scenario/test/instance.rb b/qa/qa/scenario/test/instance.rb index dcd0a32d79d..1557fdeff34 100644 --- a/qa/qa/scenario/test/instance.rb +++ b/qa/qa/scenario/test/instance.rb @@ -12,8 +12,9 @@ module QA end ## - # Temporary CE + EE support - Scenario::Gitlab::License::Add.perform if tag.to_s == 'ee' + # Perform before hooks, which are different for CE and EE + # + Runtime::Release.perform_before_hooks Specs::Runner.perform do |specs| files = files.any? ? files : 'qa/specs/features' -- cgit v1.2.1 From 8a418f3e487cf7d89369ae4ef355c334c89ac6da Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 9 Mar 2017 12:34:23 +0100 Subject: Remove unused tags in GitLab QA feature specs [ci skip] --- qa/qa/scenario/test/instance.rb | 6 ++---- qa/qa/specs/features/login/standard_spec.rb | 2 +- qa/qa/specs/features/project/create_spec.rb | 2 +- qa/qa/specs/features/repository/clone_spec.rb | 2 +- qa/qa/specs/features/repository/push_spec.rb | 2 +- 5 files changed, 6 insertions(+), 8 deletions(-) diff --git a/qa/qa/scenario/test/instance.rb b/qa/qa/scenario/test/instance.rb index 1557fdeff34..689292bc60b 100644 --- a/qa/qa/scenario/test/instance.rb +++ b/qa/qa/scenario/test/instance.rb @@ -6,7 +6,7 @@ module QA # including staging and on-premises installation. # class Instance < Scenario::Template - def perform(address, tag, *files) + def perform(address, *files) Specs::Config.perform do |specs| specs.address = address end @@ -17,9 +17,7 @@ module QA Runtime::Release.perform_before_hooks Specs::Runner.perform do |specs| - files = files.any? ? files : 'qa/specs/features' - - specs.rspec('--tty', '--tag', tag.to_s, files) + specs.rspec('--tty', files.any? ? files : 'qa/specs/features') end end end diff --git a/qa/qa/specs/features/login/standard_spec.rb b/qa/qa/specs/features/login/standard_spec.rb index ecb3f0cb68c..8e1ae6efa47 100644 --- a/qa/qa/specs/features/login/standard_spec.rb +++ b/qa/qa/specs/features/login/standard_spec.rb @@ -1,5 +1,5 @@ module QA - feature 'standard root login', :ce, :ee do + feature 'standard root login' do scenario 'user logs in using credentials' do Page::Main::Entry.act { sign_in_using_credentials } diff --git a/qa/qa/specs/features/project/create_spec.rb b/qa/qa/specs/features/project/create_spec.rb index cf4226252a6..610492b9717 100644 --- a/qa/qa/specs/features/project/create_spec.rb +++ b/qa/qa/specs/features/project/create_spec.rb @@ -1,5 +1,5 @@ module QA - feature 'create a new project', :ce, :ee, :staging do + feature 'create a new project' do scenario 'user creates a new project' do Page::Main::Entry.act { sign_in_using_credentials } diff --git a/qa/qa/specs/features/repository/clone_spec.rb b/qa/qa/specs/features/repository/clone_spec.rb index a772dc227e3..521bd955857 100644 --- a/qa/qa/specs/features/repository/clone_spec.rb +++ b/qa/qa/specs/features/repository/clone_spec.rb @@ -1,5 +1,5 @@ module QA - feature 'clone code from the repository', :ce, :ee, :staging do + feature 'clone code from the repository' do context 'with regular account over http' do given(:location) do Page::Project::Show.act do diff --git a/qa/qa/specs/features/repository/push_spec.rb b/qa/qa/specs/features/repository/push_spec.rb index 4b6cb7908bb..5fe45d63d37 100644 --- a/qa/qa/specs/features/repository/push_spec.rb +++ b/qa/qa/specs/features/repository/push_spec.rb @@ -1,5 +1,5 @@ module QA - feature 'push code to repository', :ce, :ee, :staging do + feature 'push code to repository' do context 'with regular account over http' do scenario 'user pushes code to the repository' do Page::Main::Entry.act { sign_in_using_credentials } -- cgit v1.2.1 From 534f02b9340d30d9bb751b429ffdfc6351abc1a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Sad=C5=82ocha?= Date: Thu, 9 Mar 2017 11:40:08 +0000 Subject: Fix miswording --- doc/ci/ssh_keys/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md index 49e7ac38b26..d00faaadc8b 100644 --- a/doc/ci/ssh_keys/README.md +++ b/doc/ci/ssh_keys/README.md @@ -30,8 +30,8 @@ This is the universal solution which works with any type of executor ## SSH keys when using the Docker executor You will first need to create an SSH key pair. For more information, follow the -instructions to [generate an SSH key](../../ssh/README.md). Do not add a comment -to the SSH key, or the `before_script` will prompt for a passphrase. +instructions to [generate an SSH key](../../ssh/README.md). Do not add a +passphrase to the SSH key, or the `before_script` will prompt for it. Then, create a new **Secret Variable** in your project settings on GitLab following **Settings > Variables**. As **Key** add the name `SSH_PRIVATE_KEY` -- cgit v1.2.1 From 7eabb7a9641481d89ccb52b421dcbd8cd63c3bb6 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 9 Mar 2017 12:32:43 +0000 Subject: Use reduce instead of a forEach Changed an isArray check to use -1 Added comment to boards search manager to explain behaviour --- app/assets/javascripts/boards/boards_bundle.js | 15 +++++++++++++-- app/assets/javascripts/boards/components/board_card.js | 3 ++- .../javascripts/boards/components/issue_card_inner.js | 10 +++++++++- app/assets/javascripts/boards/filtered_search_boards.js | 3 +++ app/assets/javascripts/boards/models/list.js | 14 +++++++------- 5 files changed, 34 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js index 2fd1f43f02c..4d60fedaeb8 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -62,7 +62,13 @@ $(() => { created () { gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId); - gl.boardsFilterManager = new FilteredSearchBoards(Store.filter, true); + this.filterManager = new FilteredSearchBoards(Store.filter, true); + + // Listen for updateTokens event + this.$on('updateTokens', this.updateTokens); + }, + beforeDestroy() { + this.$off('updateTokens', this.updateTokens); }, mounted () { Store.disabled = this.disabled; @@ -81,7 +87,12 @@ $(() => { Store.addBlankState(); this.loading = false; }); - } + }, + methods: { + updateTokens() { + this.filterManager.updateTokens(); + } + }, }); gl.IssueBoardsSearch = new Vue({ diff --git a/app/assets/javascripts/boards/components/board_card.js b/app/assets/javascripts/boards/components/board_card.js index 795b3cf2ec0..4b72090df31 100644 --- a/app/assets/javascripts/boards/components/board_card.js +++ b/app/assets/javascripts/boards/components/board_card.js @@ -17,7 +17,8 @@ export default { :list="list" :issue="issue" :issue-link-base="issueLinkBase" - :root-path="rootPath" /> + :root-path="rootPath" + :update-filters="true" /> `, components: { diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js index dce573ed6ca..3d57ec429c6 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.js +++ b/app/assets/javascripts/boards/components/issue_card_inner.js @@ -23,6 +23,11 @@ type: String, required: true, }, + updateFilters: { + type: Boolean, + required: false, + default: false, + }, }, methods: { showLabel(label) { @@ -31,6 +36,8 @@ return !this.list.label || label.id !== this.list.label.id; }, filterByLabel(label, e) { + if (!this.updateFilters) return; + const filterPath = gl.issueBoards.BoardsStore.filter.path.split('&'); const labelTitle = encodeURIComponent(label.title); const param = `label_name[]=${labelTitle}`; @@ -46,7 +53,8 @@ gl.issueBoards.BoardsStore.filter.path = filterPath.join('&'); Store.updateFiltersUrl(); - gl.boardsFilterManager.updateTokens(); + + gl.IssueBoardsApp.$emit('updateTokens'); }, labelStyle(label) { return { diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js index 3014557c440..47448b02bdd 100644 --- a/app/assets/javascripts/boards/filtered_search_boards.js +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -4,6 +4,9 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { this.store = store; this.updateUrl = updateUrl; + + // Issue boards is slightly different, we handle all the requests async + // instead or reloading the page, we just re-fire the list ajax requests this.isHandledAsync = true; } diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index ad968d2120f..3251ca76b26 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -64,16 +64,14 @@ class List { } getIssues (emptyIssues = true) { - const data = { page: this.page }; - gl.issueBoards.BoardsStore.filter.path.split('&').forEach((filterParam) => { - if (filterParam === '') return; + const data = gl.issueBoards.BoardsStore.filter.path.split('&').reduce((data, filterParam) => { + if (filterParam === '') return data; const paramSplit = filterParam.split('='); const paramKeyNormalized = paramSplit[0].replace('[]', ''); const isArray = paramSplit[0].indexOf('[]'); - let value = decodeURIComponent(paramSplit[1]); - value = value.replace(/\+/g, ' '); + const value = decodeURIComponent(paramSplit[1]).replace(/\+/g, ' '); - if (isArray >= 0) { + if (isArray !== -1) { if (!data[paramKeyNormalized]) { data[paramKeyNormalized] = []; } @@ -82,7 +80,9 @@ class List { } else { data[paramKeyNormalized] = value; } - }); + + return data; + }, { page: this.page }); if (this.label && data.label_name) { data.label_name = data.label_name.filter(label => label !== this.label.title); -- cgit v1.2.1 From 175a3dfda00fb5a2bf1703803277ee4abb721baf Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 9 Mar 2017 15:04:05 +0100 Subject: Fix GitLab QA release inflector strategy --- qa/qa/runtime/release.rb | 2 +- qa/spec/runtime/release_spec.rb | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/qa/qa/runtime/release.rb b/qa/qa/runtime/release.rb index d64b478a41a..e3da00a1881 100644 --- a/qa/qa/runtime/release.rb +++ b/qa/qa/runtime/release.rb @@ -19,7 +19,7 @@ module QA end begin - require "#{version.downcase}/strategy" + require "qa/#{version.downcase}/strategy" rescue LoadError # noop end diff --git a/qa/spec/runtime/release_spec.rb b/qa/spec/runtime/release_spec.rb index 4995ad48ee6..97f0b7e3c89 100644 --- a/qa/spec/runtime/release_spec.rb +++ b/qa/spec/runtime/release_spec.rb @@ -5,6 +5,7 @@ describe QA::Runtime::Release do before do stub_const('QA::CE::Strategy', strategy) + stub_const('QA::EE::Strategy', strategy) end describe '#has_strategy?' do @@ -32,6 +33,11 @@ describe QA::Runtime::Release do context 'when release version does not have extension strategy' do subject { described_class.new('CE') } + before do + hide_const('QA::CE::Strategy') + hide_const('QA::EE::Strategy') + end + describe '#has_strategy?' do it 'returns false' do expect(subject.has_strategy?).to be false -- cgit v1.2.1 From 3f5919e2c44ac7b18f06647342476ad5c3d757ba Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 9 Mar 2017 11:55:18 -0600 Subject: Add frequently used emojis back to awards menu Thanks @filipa for the shout` --- app/assets/javascripts/awards_handler.js | 30 ++++++------- .../add-frequently-used-emojis-back-to-menu.yml | 4 ++ spec/javascripts/awards_handler_spec.js | 50 +++++++++++++++++++--- 3 files changed, 61 insertions(+), 23 deletions(-) create mode 100644 changelogs/unreleased/add-frequently-used-emojis-back-to-menu.yml diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 54836efdf29..8a077f0081a 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -45,12 +45,12 @@ function buildCategoryMap() { }); } -function renderCategory(name, emojiList) { +function renderCategory(name, emojiList, opts = {}) { return `
${name}
-
    +
      ${emojiList.map(emojiName => `
    • "; - project_uploads_path = window.project_uploads_path || null; - max_file_size = gon.max_file_size || 10; - form_textarea = $(form).find(".js-gfm-input"); - form_textarea.wrap("
      "); - form_textarea.on('paste', (function(_this) { - return function(event) { - return handlePaste(event); - }; - })(this)); - $mdArea = $(form_textarea).closest('.md-area'); - $(form).setupMarkdownPreview(); - form_dropzone = $(form).find('.div-dropzone'); - form_dropzone.parent().addClass("div-dropzone-wrapper"); - form_dropzone.append(divHover); - form_dropzone.find(".div-dropzone-hover").append(iconPaperclip); - form_dropzone.append(divSpinner); - form_dropzone.find(".div-dropzone-spinner").append(iconSpinner); - form_dropzone.find(".div-dropzone-spinner").append(uploadProgress); - form_dropzone.find(".div-dropzone-spinner").css({ - "opacity": 0, - "display": "none" - }); - dropzone = form_dropzone.dropzone({ - url: project_uploads_path, - dictDefaultMessage: "", - clickable: true, - paramName: "file", - maxFilesize: max_file_size, - uploadMultiple: false, - headers: { - "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") - }, - previewContainer: false, - processing: function() { - return $(".div-dropzone-alert").alert("close"); - }, - dragover: function() { - $mdArea.addClass('is-dropzone-hover'); - form.find(".div-dropzone-hover").css("opacity", 0.7); - }, - dragleave: function() { - $mdArea.removeClass('is-dropzone-hover'); - form.find(".div-dropzone-hover").css("opacity", 0); - }, - drop: function() { - $mdArea.removeClass('is-dropzone-hover'); - form.find(".div-dropzone-hover").css("opacity", 0); - form_textarea.focus(); - }, - success: function(header, response) { - pasteText(response.link.markdown); - }, - error: function(temp) { - var checkIfMsgExists, errorAlert; - errorAlert = $(form).find('.error-alert'); - checkIfMsgExists = errorAlert.children().length; - if (checkIfMsgExists === 0) { - errorAlert.append(divAlert); - $(".div-dropzone-alert").append(btnAlert + "Attaching the file failed."); - } - }, - totaluploadprogress: function(totalUploadProgress) { - uploadProgress.text(Math.round(totalUploadProgress) + "%"); - }, - sending: function() { - form_dropzone.find(".div-dropzone-spinner").css({ - "opacity": 0.7, - "display": "inherit" - }); - }, - queuecomplete: function() { - uploadProgress.text(""); - $(".dz-preview").remove(); - $(".markdown-area").trigger("input"); - $(".div-dropzone-spinner").css({ - "opacity": 0, - "display": "none" - }); - } - }); - child = $(dropzone[0]).children("textarea"); - handlePaste = function(event) { - var filename, image, pasteEvent, text; - pasteEvent = event.originalEvent; - if (pasteEvent.clipboardData && pasteEvent.clipboardData.items) { - image = isImage(pasteEvent); - if (image) { - event.preventDefault(); - filename = getFilename(pasteEvent) || "image.png"; - text = "{{" + filename + "}}"; - pasteText(text); - return uploadFile(image.getAsFile(), filename); - } - } +window.DropzoneInput = (function() { + function DropzoneInput(form) { + var $mdArea, alertAttr, alertClass, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divAlert, divHover, divSpinner, dropzone, form_dropzone, form_textarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, max_file_size, pasteText, project_uploads_path, showError, showSpinner, uploadFile, uploadProgress; + Dropzone.autoDiscover = false; + alertClass = "alert alert-danger alert-dismissable div-dropzone-alert"; + alertAttr = "class=\"close\" data-dismiss=\"alert\"" + "aria-hidden=\"true\""; + divHover = "
      "; + divSpinner = "
      "; + divAlert = "
      "; + iconPaperclip = ""; + iconSpinner = ""; + uploadProgress = $("
      "); + btnAlert = ""; + project_uploads_path = window.project_uploads_path || null; + max_file_size = gon.max_file_size || 10; + form_textarea = $(form).find(".js-gfm-input"); + form_textarea.wrap("
      "); + form_textarea.on('paste', (function(_this) { + return function(event) { + return handlePaste(event); }; - isImage = function(data) { - var i, item; - i = 0; - while (i < data.clipboardData.items.length) { - item = data.clipboardData.items[i]; - if (item.type.indexOf("image") !== -1) { - return item; - } - i += 1; - } - return false; - }; - pasteText = function(text) { - var afterSelection, beforeSelection, caretEnd, caretStart, textEnd; - var formattedText = text + "\n\n"; - caretStart = $(child)[0].selectionStart; - caretEnd = $(child)[0].selectionEnd; - textEnd = $(child).val().length; - beforeSelection = $(child).val().substring(0, caretStart); - afterSelection = $(child).val().substring(caretEnd, textEnd); - $(child).val(beforeSelection + formattedText + afterSelection); - child.get(0).setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length); - return form_textarea.trigger("input"); - }; - getFilename = function(e) { - var value; - if (window.clipboardData && window.clipboardData.getData) { - value = window.clipboardData.getData("Text"); - } else if (e.clipboardData && e.clipboardData.getData) { - value = e.clipboardData.getData("text/plain"); + })(this)); + $mdArea = $(form_textarea).closest('.md-area'); + $(form).setupMarkdownPreview(); + form_dropzone = $(form).find('.div-dropzone'); + form_dropzone.parent().addClass("div-dropzone-wrapper"); + form_dropzone.append(divHover); + form_dropzone.find(".div-dropzone-hover").append(iconPaperclip); + form_dropzone.append(divSpinner); + form_dropzone.find(".div-dropzone-spinner").append(iconSpinner); + form_dropzone.find(".div-dropzone-spinner").append(uploadProgress); + form_dropzone.find(".div-dropzone-spinner").css({ + "opacity": 0, + "display": "none" + }); + dropzone = form_dropzone.dropzone({ + url: project_uploads_path, + dictDefaultMessage: "", + clickable: true, + paramName: "file", + maxFilesize: max_file_size, + uploadMultiple: false, + headers: { + "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") + }, + previewContainer: false, + processing: function() { + return $(".div-dropzone-alert").alert("close"); + }, + dragover: function() { + $mdArea.addClass('is-dropzone-hover'); + form.find(".div-dropzone-hover").css("opacity", 0.7); + }, + dragleave: function() { + $mdArea.removeClass('is-dropzone-hover'); + form.find(".div-dropzone-hover").css("opacity", 0); + }, + drop: function() { + $mdArea.removeClass('is-dropzone-hover'); + form.find(".div-dropzone-hover").css("opacity", 0); + form_textarea.focus(); + }, + success: function(header, response) { + pasteText(response.link.markdown); + }, + error: function(temp) { + var checkIfMsgExists, errorAlert; + errorAlert = $(form).find('.error-alert'); + checkIfMsgExists = errorAlert.children().length; + if (checkIfMsgExists === 0) { + errorAlert.append(divAlert); + $(".div-dropzone-alert").append(btnAlert + "Attaching the file failed."); } - value = value.split("\r"); - return value.first(); - }; - uploadFile = function(item, filename) { - var formData; - formData = new FormData(); - formData.append("file", item, filename); - return $.ajax({ - url: project_uploads_path, - type: "POST", - data: formData, - dataType: "json", - processData: false, - contentType: false, - headers: { - "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") - }, - beforeSend: function() { - showSpinner(); - return closeAlertMessage(); - }, - success: function(e, textStatus, response) { - return insertToTextArea(filename, response.responseJSON.link.markdown); - }, - error: function(response) { - return showError(response.responseJSON.message); - }, - complete: function() { - return closeSpinner(); - } - }); - }; - insertToTextArea = function(filename, url) { - return $(child).val(function(index, val) { - return val.replace("{{" + filename + "}}", url + "\n"); - }); - }; - appendToTextArea = function(url) { - return $(child).val(function(index, val) { - return val + url + "\n"; - }); - }; - showSpinner = function(e) { - return form.find(".div-dropzone-spinner").css({ + }, + totaluploadprogress: function(totalUploadProgress) { + uploadProgress.text(Math.round(totalUploadProgress) + "%"); + }, + sending: function() { + form_dropzone.find(".div-dropzone-spinner").css({ "opacity": 0.7, "display": "inherit" }); - }; - closeSpinner = function() { - return form.find(".div-dropzone-spinner").css({ + }, + queuecomplete: function() { + uploadProgress.text(""); + $(".dz-preview").remove(); + $(".markdown-area").trigger("input"); + $(".div-dropzone-spinner").css({ "opacity": 0, "display": "none" }); - }; - showError = function(message) { - var checkIfMsgExists, errorAlert; - errorAlert = $(form).find('.error-alert'); - checkIfMsgExists = errorAlert.children().length; - if (checkIfMsgExists === 0) { - errorAlert.append(divAlert); - return $(".div-dropzone-alert").append(btnAlert + message); + } + }); + child = $(dropzone[0]).children("textarea"); + handlePaste = function(event) { + var filename, image, pasteEvent, text; + pasteEvent = event.originalEvent; + if (pasteEvent.clipboardData && pasteEvent.clipboardData.items) { + image = isImage(pasteEvent); + if (image) { + event.preventDefault(); + filename = getFilename(pasteEvent) || "image.png"; + text = "{{" + filename + "}}"; + pasteText(text); + return uploadFile(image.getAsFile(), filename); } - }; - closeAlertMessage = function() { - return form.find(".div-dropzone-alert").alert("close"); - }; - form.find(".markdown-selector").click(function(e) { - e.preventDefault(); - $(this).closest('.gfm-form').find('.div-dropzone').click(); + } + }; + isImage = function(data) { + var i, item; + i = 0; + while (i < data.clipboardData.items.length) { + item = data.clipboardData.items[i]; + if (item.type.indexOf("image") !== -1) { + return item; + } + i += 1; + } + return false; + }; + pasteText = function(text) { + var afterSelection, beforeSelection, caretEnd, caretStart, textEnd; + var formattedText = text + "\n\n"; + caretStart = $(child)[0].selectionStart; + caretEnd = $(child)[0].selectionEnd; + textEnd = $(child).val().length; + beforeSelection = $(child).val().substring(0, caretStart); + afterSelection = $(child).val().substring(caretEnd, textEnd); + $(child).val(beforeSelection + formattedText + afterSelection); + child.get(0).setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length); + return form_textarea.trigger("input"); + }; + getFilename = function(e) { + var value; + if (window.clipboardData && window.clipboardData.getData) { + value = window.clipboardData.getData("Text"); + } else if (e.clipboardData && e.clipboardData.getData) { + value = e.clipboardData.getData("text/plain"); + } + value = value.split("\r"); + return value.first(); + }; + uploadFile = function(item, filename) { + var formData; + formData = new FormData(); + formData.append("file", item, filename); + return $.ajax({ + url: project_uploads_path, + type: "POST", + data: formData, + dataType: "json", + processData: false, + contentType: false, + headers: { + "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") + }, + beforeSend: function() { + showSpinner(); + return closeAlertMessage(); + }, + success: function(e, textStatus, response) { + return insertToTextArea(filename, response.responseJSON.link.markdown); + }, + error: function(response) { + return showError(response.responseJSON.message); + }, + complete: function() { + return closeSpinner(); + } + }); + }; + insertToTextArea = function(filename, url) { + return $(child).val(function(index, val) { + return val.replace("{{" + filename + "}}", url + "\n"); + }); + }; + appendToTextArea = function(url) { + return $(child).val(function(index, val) { + return val + url + "\n"; + }); + }; + showSpinner = function(e) { + return form.find(".div-dropzone-spinner").css({ + "opacity": 0.7, + "display": "inherit" + }); + }; + closeSpinner = function() { + return form.find(".div-dropzone-spinner").css({ + "opacity": 0, + "display": "none" }); - } + }; + showError = function(message) { + var checkIfMsgExists, errorAlert; + errorAlert = $(form).find('.error-alert'); + checkIfMsgExists = errorAlert.children().length; + if (checkIfMsgExists === 0) { + errorAlert.append(divAlert); + return $(".div-dropzone-alert").append(btnAlert + message); + } + }; + closeAlertMessage = function() { + return form.find(".div-dropzone-alert").alert("close"); + }; + form.find(".markdown-selector").click(function(e) { + e.preventDefault(); + $(this).closest('.gfm-form').find('.div-dropzone').click(); + }); + } - return DropzoneInput; - })(); -}).call(window); + return DropzoneInput; +})(); diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js index 9169fcd7328..fdbb4644971 100644 --- a/app/assets/javascripts/due_date_select.js +++ b/app/assets/javascripts/due_date_select.js @@ -2,203 +2,202 @@ /* global dateFormat */ /* global Pikaday */ -(function(global) { - class DueDateSelect { - constructor({ $dropdown, $loading } = {}) { - const $dropdownParent = $dropdown.closest('.dropdown'); - const $block = $dropdown.closest('.block'); - this.$loading = $loading; - this.$dropdown = $dropdown; - this.$dropdownParent = $dropdownParent; - this.$datePicker = $dropdownParent.find('.js-due-date-calendar'); - this.$block = $block; - this.$selectbox = $dropdown.closest('.selectbox'); - this.$value = $block.find('.value'); - this.$valueContent = $block.find('.value-content'); - this.$sidebarValue = $('.js-due-date-sidebar-value', $block); - this.fieldName = $dropdown.data('field-name'), - this.abilityName = $dropdown.data('ability-name'), - this.issueUpdateURL = $dropdown.data('issue-update'); - - this.rawSelectedDate = null; - this.displayedDate = null; - this.datePayload = null; - - this.initGlDropdown(); - this.initRemoveDueDate(); - this.initDatePicker(); - } - - initGlDropdown() { - this.$dropdown.glDropdown({ - opened: () => { - const calendar = this.$datePicker.data('pikaday'); - calendar.show(); - }, - hidden: () => { - this.$selectbox.hide(); - this.$value.css('display', ''); - } - }); - } - - initDatePicker() { - const $dueDateInput = $(`input[name='${this.fieldName}']`); - - const calendar = new Pikaday({ - field: $dueDateInput.get(0), - theme: 'gitlab-theme', - format: 'yyyy-mm-dd', - onSelect: (dateText) => { - const formattedDate = dateFormat(new Date(dateText), 'yyyy-mm-dd'); - - $dueDateInput.val(formattedDate); +class DueDateSelect { + constructor({ $dropdown, $loading } = {}) { + const $dropdownParent = $dropdown.closest('.dropdown'); + const $block = $dropdown.closest('.block'); + this.$loading = $loading; + this.$dropdown = $dropdown; + this.$dropdownParent = $dropdownParent; + this.$datePicker = $dropdownParent.find('.js-due-date-calendar'); + this.$block = $block; + this.$selectbox = $dropdown.closest('.selectbox'); + this.$value = $block.find('.value'); + this.$valueContent = $block.find('.value-content'); + this.$sidebarValue = $('.js-due-date-sidebar-value', $block); + this.fieldName = $dropdown.data('field-name'), + this.abilityName = $dropdown.data('ability-name'), + this.issueUpdateURL = $dropdown.data('issue-update'); + + this.rawSelectedDate = null; + this.displayedDate = null; + this.datePayload = null; + + this.initGlDropdown(); + this.initRemoveDueDate(); + this.initDatePicker(); + } - if (this.$dropdown.hasClass('js-issue-boards-due-date')) { - gl.issueBoards.BoardsStore.detail.issue.dueDate = $dueDateInput.val(); - this.updateIssueBoardIssue(); - } else { - this.saveDueDate(true); - } - } - }); + initGlDropdown() { + this.$dropdown.glDropdown({ + opened: () => { + const calendar = this.$datePicker.data('pikaday'); + calendar.show(); + }, + hidden: () => { + this.$selectbox.hide(); + this.$value.css('display', ''); + } + }); + } - calendar.setDate(new Date($dueDateInput.val())); - this.$datePicker.append(calendar.el); - this.$datePicker.data('pikaday', calendar); - } + initDatePicker() { + const $dueDateInput = $(`input[name='${this.fieldName}']`); - initRemoveDueDate() { - this.$block.on('click', '.js-remove-due-date', (e) => { - const calendar = this.$datePicker.data('pikaday'); - e.preventDefault(); + const calendar = new Pikaday({ + field: $dueDateInput.get(0), + theme: 'gitlab-theme', + format: 'yyyy-mm-dd', + onSelect: (dateText) => { + const formattedDate = dateFormat(new Date(dateText), 'yyyy-mm-dd'); - calendar.setDate(null); + $dueDateInput.val(formattedDate); if (this.$dropdown.hasClass('js-issue-boards-due-date')) { - gl.issueBoards.BoardsStore.detail.issue.dueDate = ''; + gl.issueBoards.BoardsStore.detail.issue.dueDate = $dueDateInput.val(); this.updateIssueBoardIssue(); } else { - $("input[name='" + this.fieldName + "']").val(''); - return this.saveDueDate(false); + this.saveDueDate(true); } - }); - } + } + }); - saveDueDate(isDropdown) { - this.parseSelectedDate(); - this.prepSelectedDate(); - this.submitSelectedDate(isDropdown); - } + calendar.setDate(new Date($dueDateInput.val())); + this.$datePicker.append(calendar.el); + this.$datePicker.data('pikaday', calendar); + } + + initRemoveDueDate() { + this.$block.on('click', '.js-remove-due-date', (e) => { + const calendar = this.$datePicker.data('pikaday'); + e.preventDefault(); - parseSelectedDate() { - this.rawSelectedDate = $(`input[name='${this.fieldName}']`).val(); + calendar.setDate(null); - if (this.rawSelectedDate.length) { - // Construct Date object manually to avoid buggy dateString support within Date constructor - const dateArray = this.rawSelectedDate.split('-').map(v => parseInt(v, 10)); - const dateObj = new Date(dateArray[0], dateArray[1] - 1, dateArray[2]); - this.displayedDate = dateFormat(dateObj, 'mmm d, yyyy'); + if (this.$dropdown.hasClass('js-issue-boards-due-date')) { + gl.issueBoards.BoardsStore.detail.issue.dueDate = ''; + this.updateIssueBoardIssue(); } else { - this.displayedDate = 'No due date'; + $("input[name='" + this.fieldName + "']").val(''); + return this.saveDueDate(false); } - } + }); + } - prepSelectedDate() { - const datePayload = {}; - datePayload[this.abilityName] = {}; - datePayload[this.abilityName].due_date = this.rawSelectedDate; - this.datePayload = datePayload; - } + saveDueDate(isDropdown) { + this.parseSelectedDate(); + this.prepSelectedDate(); + this.submitSelectedDate(isDropdown); + } - updateIssueBoardIssue () { - this.$loading.fadeIn(); - this.$dropdown.trigger('loading.gl.dropdown'); - this.$selectbox.hide(); - this.$value.css('display', ''); + parseSelectedDate() { + this.rawSelectedDate = $(`input[name='${this.fieldName}']`).val(); - gl.issueBoards.BoardsStore.detail.issue.update(this.$dropdown.attr('data-issue-update')) - .then(() => { - this.$loading.fadeOut(); - }); + if (this.rawSelectedDate.length) { + // Construct Date object manually to avoid buggy dateString support within Date constructor + const dateArray = this.rawSelectedDate.split('-').map(v => parseInt(v, 10)); + const dateObj = new Date(dateArray[0], dateArray[1] - 1, dateArray[2]); + this.displayedDate = dateFormat(dateObj, 'mmm d, yyyy'); + } else { + this.displayedDate = 'No due date'; } + } + + prepSelectedDate() { + const datePayload = {}; + datePayload[this.abilityName] = {}; + datePayload[this.abilityName].due_date = this.rawSelectedDate; + this.datePayload = datePayload; + } + + updateIssueBoardIssue () { + this.$loading.fadeIn(); + this.$dropdown.trigger('loading.gl.dropdown'); + this.$selectbox.hide(); + this.$value.css('display', ''); + + gl.issueBoards.BoardsStore.detail.issue.update(this.$dropdown.attr('data-issue-update')) + .then(() => { + this.$loading.fadeOut(); + }); + } + + submitSelectedDate(isDropdown) { + return $.ajax({ + type: 'PUT', + url: this.issueUpdateURL, + data: this.datePayload, + dataType: 'json', + beforeSend: () => { + const selectedDateValue = this.datePayload[this.abilityName].due_date; + const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value'; + + this.$loading.fadeIn(); - submitSelectedDate(isDropdown) { - return $.ajax({ - type: 'PUT', - url: this.issueUpdateURL, - data: this.datePayload, - dataType: 'json', - beforeSend: () => { - const selectedDateValue = this.datePayload[this.abilityName].due_date; - const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value'; - - this.$loading.fadeIn(); - - if (isDropdown) { - this.$dropdown.trigger('loading.gl.dropdown'); - this.$selectbox.hide(); - } - - this.$value.css('display', ''); - this.$valueContent.html(`${this.displayedDate}`); - this.$sidebarValue.html(this.displayedDate); - - return selectedDateValue.length ? - $('.js-remove-due-date-holder').removeClass('hidden') : - $('.js-remove-due-date-holder').addClass('hidden'); - } - }).done((data) => { if (isDropdown) { - this.$dropdown.trigger('loaded.gl.dropdown'); - this.$dropdown.dropdown('toggle'); + this.$dropdown.trigger('loading.gl.dropdown'); + this.$selectbox.hide(); } - return this.$loading.fadeOut(); - }); - } + + this.$value.css('display', ''); + this.$valueContent.html(`${this.displayedDate}`); + this.$sidebarValue.html(this.displayedDate); + + return selectedDateValue.length ? + $('.js-remove-due-date-holder').removeClass('hidden') : + $('.js-remove-due-date-holder').addClass('hidden'); + } + }).done((data) => { + if (isDropdown) { + this.$dropdown.trigger('loaded.gl.dropdown'); + this.$dropdown.dropdown('toggle'); + } + return this.$loading.fadeOut(); + }); } +} - class DueDateSelectors { - constructor() { - this.initMilestoneDatePicker(); - this.initIssuableSelect(); - } +class DueDateSelectors { + constructor() { + this.initMilestoneDatePicker(); + this.initIssuableSelect(); + } - initMilestoneDatePicker() { - $('.datepicker').each(function() { - const $datePicker = $(this); - const calendar = new Pikaday({ - field: $datePicker.get(0), - theme: 'gitlab-theme', - format: 'yyyy-mm-dd', - onSelect(dateText) { - $datePicker.val(dateFormat(new Date(dateText), 'yyyy-mm-dd')); - } - }); - calendar.setDate(new Date($datePicker.val())); - - $datePicker.data('pikaday', calendar); + initMilestoneDatePicker() { + $('.datepicker').each(function() { + const $datePicker = $(this); + const calendar = new Pikaday({ + field: $datePicker.get(0), + theme: 'gitlab-theme', + format: 'yyyy-mm-dd', + onSelect(dateText) { + $datePicker.val(dateFormat(new Date(dateText), 'yyyy-mm-dd')); + } }); + calendar.setDate(new Date($datePicker.val())); - $('.js-clear-due-date,.js-clear-start-date').on('click', (e) => { - e.preventDefault(); - const calendar = $(e.target).siblings('.datepicker').data('pikaday'); - calendar.setDate(null); - }); - } + $datePicker.data('pikaday', calendar); + }); - initIssuableSelect() { - const $loading = $('.js-issuable-update .due_date').find('.block-loading').hide(); + $('.js-clear-due-date,.js-clear-start-date').on('click', (e) => { + e.preventDefault(); + const calendar = $(e.target).siblings('.datepicker').data('pikaday'); + calendar.setDate(null); + }); + } + + initIssuableSelect() { + const $loading = $('.js-issuable-update .due_date').find('.block-loading').hide(); - $('.js-due-date-select').each((i, dropdown) => { - const $dropdown = $(dropdown); - new DueDateSelect({ - $dropdown, - $loading - }); + $('.js-due-date-select').each((i, dropdown) => { + const $dropdown = $(dropdown); + new DueDateSelect({ + $dropdown, + $loading }); - } + }); } +} - global.DueDateSelectors = DueDateSelectors; -})(window.gl || (window.gl = {})); +window.gl = window.gl || {}; +window.gl.DueDateSelectors = DueDateSelectors; diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js index bf84f2a0a8f..3f041172ff3 100644 --- a/app/assets/javascripts/files_comment_button.js +++ b/app/assets/javascripts/files_comment_button.js @@ -2,142 +2,140 @@ /* global FilesCommentButton */ /* global notes */ -(function() { - let $commentButtonTemplate; - var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; +let $commentButtonTemplate; +var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; - this.FilesCommentButton = (function() { - var COMMENT_BUTTON_CLASS, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS; +window.FilesCommentButton = (function() { + var COMMENT_BUTTON_CLASS, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS; - COMMENT_BUTTON_CLASS = '.add-diff-note'; + COMMENT_BUTTON_CLASS = '.add-diff-note'; - LINE_HOLDER_CLASS = '.line_holder'; + LINE_HOLDER_CLASS = '.line_holder'; - LINE_NUMBER_CLASS = 'diff-line-num'; + LINE_NUMBER_CLASS = 'diff-line-num'; - LINE_CONTENT_CLASS = 'line_content'; + LINE_CONTENT_CLASS = 'line_content'; - UNFOLDABLE_LINE_CLASS = 'js-unfold'; + UNFOLDABLE_LINE_CLASS = 'js-unfold'; - EMPTY_CELL_CLASS = 'empty-cell'; + EMPTY_CELL_CLASS = 'empty-cell'; - OLD_LINE_CLASS = 'old_line'; + OLD_LINE_CLASS = 'old_line'; - LINE_COLUMN_CLASSES = "." + LINE_NUMBER_CLASS + ", .line_content"; + LINE_COLUMN_CLASSES = "." + LINE_NUMBER_CLASS + ", .line_content"; - TEXT_FILE_SELECTOR = '.text-file'; + TEXT_FILE_SELECTOR = '.text-file'; - function FilesCommentButton(filesContainerElement) { - this.render = bind(this.render, this); - this.hideButton = bind(this.hideButton, this); - this.isParallelView = notes.isParallelView(); - filesContainerElement.on('mouseover', LINE_COLUMN_CLASSES, this.render) - .on('mouseleave', LINE_COLUMN_CLASSES, this.hideButton); + function FilesCommentButton(filesContainerElement) { + this.render = bind(this.render, this); + this.hideButton = bind(this.hideButton, this); + this.isParallelView = notes.isParallelView(); + filesContainerElement.on('mouseover', LINE_COLUMN_CLASSES, this.render) + .on('mouseleave', LINE_COLUMN_CLASSES, this.hideButton); + } + + FilesCommentButton.prototype.render = function(e) { + var $currentTarget, buttonParentElement, lineContentElement, textFileElement, $button; + $currentTarget = $(e.currentTarget); + + if ($currentTarget.hasClass('js-no-comment-btn')) return; + + lineContentElement = this.getLineContent($currentTarget); + buttonParentElement = this.getButtonParent($currentTarget); + + if (!this.validateButtonParent(buttonParentElement) || !this.validateLineContent(lineContentElement)) return; + + $button = $(COMMENT_BUTTON_CLASS, buttonParentElement); + buttonParentElement.addClass('is-over') + .nextUntil(`.${LINE_CONTENT_CLASS}`).addClass('is-over'); + + if ($button.length) { + return; } - FilesCommentButton.prototype.render = function(e) { - var $currentTarget, buttonParentElement, lineContentElement, textFileElement, $button; - $currentTarget = $(e.currentTarget); + textFileElement = this.getTextFileElement($currentTarget); + buttonParentElement.append(this.buildButton({ + noteableType: textFileElement.attr('data-noteable-type'), + noteableID: textFileElement.attr('data-noteable-id'), + commitID: textFileElement.attr('data-commit-id'), + noteType: lineContentElement.attr('data-note-type'), + position: lineContentElement.attr('data-position'), + lineType: lineContentElement.attr('data-line-type'), + discussionID: lineContentElement.attr('data-discussion-id'), + lineCode: lineContentElement.attr('data-line-code') + })); + }; - if ($currentTarget.hasClass('js-no-comment-btn')) return; + FilesCommentButton.prototype.hideButton = function(e) { + var $currentTarget = $(e.currentTarget); + var buttonParentElement = this.getButtonParent($currentTarget); - lineContentElement = this.getLineContent($currentTarget); - buttonParentElement = this.getButtonParent($currentTarget); + buttonParentElement.removeClass('is-over') + .nextUntil(`.${LINE_CONTENT_CLASS}`).removeClass('is-over'); + }; - if (!this.validateButtonParent(buttonParentElement) || !this.validateLineContent(lineContentElement)) return; + FilesCommentButton.prototype.buildButton = function(buttonAttributes) { + return $commentButtonTemplate.clone().attr({ + 'data-noteable-type': buttonAttributes.noteableType, + 'data-noteable-id': buttonAttributes.noteableID, + 'data-commit-id': buttonAttributes.commitID, + 'data-note-type': buttonAttributes.noteType, + 'data-line-code': buttonAttributes.lineCode, + 'data-position': buttonAttributes.position, + 'data-discussion-id': buttonAttributes.discussionID, + 'data-line-type': buttonAttributes.lineType + }); + }; - $button = $(COMMENT_BUTTON_CLASS, buttonParentElement); - buttonParentElement.addClass('is-over') - .nextUntil(`.${LINE_CONTENT_CLASS}`).addClass('is-over'); + FilesCommentButton.prototype.getTextFileElement = function(hoveredElement) { + return hoveredElement.closest(TEXT_FILE_SELECTOR); + }; - if ($button.length) { - return; - } + FilesCommentButton.prototype.getLineContent = function(hoveredElement) { + if (hoveredElement.hasClass(LINE_CONTENT_CLASS)) { + return hoveredElement; + } + if (!this.isParallelView) { + return $(hoveredElement).closest(LINE_HOLDER_CLASS).find("." + LINE_CONTENT_CLASS); + } else { + return $(hoveredElement).next("." + LINE_CONTENT_CLASS); + } + }; - textFileElement = this.getTextFileElement($currentTarget); - buttonParentElement.append(this.buildButton({ - noteableType: textFileElement.attr('data-noteable-type'), - noteableID: textFileElement.attr('data-noteable-id'), - commitID: textFileElement.attr('data-commit-id'), - noteType: lineContentElement.attr('data-note-type'), - position: lineContentElement.attr('data-position'), - lineType: lineContentElement.attr('data-line-type'), - discussionID: lineContentElement.attr('data-discussion-id'), - lineCode: lineContentElement.attr('data-line-code') - })); - }; - - FilesCommentButton.prototype.hideButton = function(e) { - var $currentTarget = $(e.currentTarget); - var buttonParentElement = this.getButtonParent($currentTarget); - - buttonParentElement.removeClass('is-over') - .nextUntil(`.${LINE_CONTENT_CLASS}`).removeClass('is-over'); - }; - - FilesCommentButton.prototype.buildButton = function(buttonAttributes) { - return $commentButtonTemplate.clone().attr({ - 'data-noteable-type': buttonAttributes.noteableType, - 'data-noteable-id': buttonAttributes.noteableID, - 'data-commit-id': buttonAttributes.commitID, - 'data-note-type': buttonAttributes.noteType, - 'data-line-code': buttonAttributes.lineCode, - 'data-position': buttonAttributes.position, - 'data-discussion-id': buttonAttributes.discussionID, - 'data-line-type': buttonAttributes.lineType - }); - }; - - FilesCommentButton.prototype.getTextFileElement = function(hoveredElement) { - return hoveredElement.closest(TEXT_FILE_SELECTOR); - }; - - FilesCommentButton.prototype.getLineContent = function(hoveredElement) { - if (hoveredElement.hasClass(LINE_CONTENT_CLASS)) { + FilesCommentButton.prototype.getButtonParent = function(hoveredElement) { + if (!this.isParallelView) { + if (hoveredElement.hasClass(OLD_LINE_CLASS)) { return hoveredElement; } - if (!this.isParallelView) { - return $(hoveredElement).closest(LINE_HOLDER_CLASS).find("." + LINE_CONTENT_CLASS); - } else { - return $(hoveredElement).next("." + LINE_CONTENT_CLASS); - } - }; - - FilesCommentButton.prototype.getButtonParent = function(hoveredElement) { - if (!this.isParallelView) { - if (hoveredElement.hasClass(OLD_LINE_CLASS)) { - return hoveredElement; - } - return hoveredElement.parent().find("." + OLD_LINE_CLASS); - } else { - if (hoveredElement.hasClass(LINE_NUMBER_CLASS)) { - return hoveredElement; - } - return $(hoveredElement).prev("." + LINE_NUMBER_CLASS); + return hoveredElement.parent().find("." + OLD_LINE_CLASS); + } else { + if (hoveredElement.hasClass(LINE_NUMBER_CLASS)) { + return hoveredElement; } - }; + return $(hoveredElement).prev("." + LINE_NUMBER_CLASS); + } + }; - FilesCommentButton.prototype.validateButtonParent = function(buttonParentElement) { - return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS); - }; + FilesCommentButton.prototype.validateButtonParent = function(buttonParentElement) { + return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS); + }; - FilesCommentButton.prototype.validateLineContent = function(lineContentElement) { - return lineContentElement.attr('data-discussion-id') && lineContentElement.attr('data-discussion-id') !== ''; - }; + FilesCommentButton.prototype.validateLineContent = function(lineContentElement) { + return lineContentElement.attr('data-discussion-id') && lineContentElement.attr('data-discussion-id') !== ''; + }; - return FilesCommentButton; - })(); + return FilesCommentButton; +})(); - $.fn.filesCommentButton = function() { - $commentButtonTemplate = $(''); +$.fn.filesCommentButton = function() { + $commentButtonTemplate = $(''); - if (!(this && (this.parent().data('can-create-note') != null))) { - return; + if (!(this && (this.parent().data('can-create-note') != null))) { + return; + } + return this.each(function() { + if (!$.data(this, 'filesCommentButton')) { + return $.data(this, 'filesCommentButton', new FilesCommentButton($(this))); } - return this.each(function() { - if (!$.data(this, 'filesCommentButton')) { - return $.data(this, 'filesCommentButton', new FilesCommentButton($(this))); - } - }); - }; -}).call(window); + }); +}; diff --git a/app/assets/javascripts/filterable_list.js b/app/assets/javascripts/filterable_list.js index 47a40e28461..aaaeb9bddb1 100644 --- a/app/assets/javascripts/filterable_list.js +++ b/app/assets/javascripts/filterable_list.js @@ -2,6 +2,7 @@ * Makes search request for content when user types a value in the search input. * Updates the html content of the page with the received one. */ + export default class FilterableList { constructor(form, filter, holder) { this.filterForm = form; diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js index 730104b89f9..eec30624ff2 100644 --- a/app/assets/javascripts/flash.js +++ b/app/assets/javascripts/flash.js @@ -1,42 +1,41 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, no-param-reassign, quotes, quote-props, prefer-template, comma-dangle, max-len */ -(function() { - this.Flash = (function() { - var hideFlash; - hideFlash = function() { - return $(this).fadeOut(); - }; +window.Flash = (function() { + var hideFlash; - function Flash(message, type, parent) { - var flash, textDiv; - if (type == null) { - type = 'alert'; - } - if (parent == null) { - parent = null; - } - if (parent) { - this.flashContainer = parent.find('.flash-container'); - } else { - this.flashContainer = $('.flash-container-page'); - } - this.flashContainer.html(''); - flash = $('
      ', { - "class": "flash-" + type - }); - flash.on('click', hideFlash); - textDiv = $('
      ', { - "class": 'flash-text', - text: message - }); - textDiv.appendTo(flash); - if (this.flashContainer.parent().hasClass('content-wrapper')) { - textDiv.addClass('container-fluid container-limited'); - } - flash.appendTo(this.flashContainer); - this.flashContainer.show(); + hideFlash = function() { + return $(this).fadeOut(); + }; + + function Flash(message, type, parent) { + var flash, textDiv; + if (type == null) { + type = 'alert'; + } + if (parent == null) { + parent = null; + } + if (parent) { + this.flashContainer = parent.find('.flash-container'); + } else { + this.flashContainer = $('.flash-container-page'); + } + this.flashContainer.html(''); + flash = $('
      ', { + "class": "flash-" + type + }); + flash.on('click', hideFlash); + textDiv = $('
      ', { + "class": 'flash-text', + text: message + }); + textDiv.appendTo(flash); + if (this.flashContainer.parent().hasClass('content-wrapper')) { + textDiv.addClass('container-fluid container-limited'); } + flash.appendTo(this.flashContainer); + this.flashContainer.show(); + } - return Flash; - })(); -}).call(window); + return Flash; +})(); diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index 4f7ce1fa197..9ac4c49d697 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -5,390 +5,386 @@ import emojiAliases from 'emojis/aliases.json'; import { glEmojiTag } from '~/behaviors/gl_emoji'; // Creates the variables for setting up GFM auto-completion -(function() { - if (window.gl == null) { - window.gl = {}; - } +window.gl = window.gl || {}; - function sanitize(str) { - return str.replace(/<(?:.|\n)*?>/gm, ''); - } +function sanitize(str) { + return str.replace(/<(?:.|\n)*?>/gm, ''); +} - window.gl.GfmAutoComplete = { - dataSources: {}, - defaultLoadingData: ['loading'], - cachedData: {}, - isLoadingData: {}, - atTypeMap: { - ':': 'emojis', - '@': 'members', - '#': 'issues', - '!': 'mergeRequests', - '~': 'labels', - '%': 'milestones', - '/': 'commands' - }, - // Emoji - Emoji: { - templateFunction: function(name) { - return `
    • - ${name} ${glEmojiTag(name)} -
    • - `; +window.gl.GfmAutoComplete = { + dataSources: {}, + defaultLoadingData: ['loading'], + cachedData: {}, + isLoadingData: {}, + atTypeMap: { + ':': 'emojis', + '@': 'members', + '#': 'issues', + '!': 'mergeRequests', + '~': 'labels', + '%': 'milestones', + '/': 'commands' + }, + // Emoji + Emoji: { + templateFunction: function(name) { + return `
    • + ${name} ${glEmojiTag(name)} +
    • + `; + } + }, + // Team Members + Members: { + template: '
    • ${avatarTag} ${username} ${title}
    • ' + }, + Labels: { + template: '
    • ${title}
    • ' + }, + // Issues and MergeRequests + Issues: { + template: '
    • ${id} ${title}
    • ' + }, + // Milestones + Milestones: { + template: '
    • ${title}
    • ' + }, + Loading: { + template: '
    • Loading...
    • ' + }, + DefaultOptions: { + sorter: function(query, items, searchKey) { + this.setting.highlightFirst = this.setting.alwaysHighlightFirst || query.length > 0; + if (gl.GfmAutoComplete.isLoading(items)) { + this.setting.highlightFirst = false; + return items; } + return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey); }, - // Team Members - Members: { - template: '
    • ${avatarTag} ${username} ${title}
    • ' - }, - Labels: { - template: '
    • ${title}
    • ' - }, - // Issues and MergeRequests - Issues: { - template: '
    • ${id} ${title}
    • ' - }, - // Milestones - Milestones: { - template: '
    • ${title}
    • ' + filter: function(query, data, searchKey) { + if (gl.GfmAutoComplete.isLoading(data)) { + gl.GfmAutoComplete.fetchData(this.$inputor, this.at); + return data; + } else { + return $.fn.atwho["default"].callbacks.filter(query, data, searchKey); + } }, - Loading: { - template: '
    • Loading...
    • ' + beforeInsert: function(value) { + if (value && !this.setting.skipSpecialCharacterTest) { + var withoutAt = value.substring(1); + if (withoutAt && /[^\w\d]/.test(withoutAt)) value = value.charAt() + '"' + withoutAt + '"'; + } + return value; }, - DefaultOptions: { - sorter: function(query, items, searchKey) { - this.setting.highlightFirst = this.setting.alwaysHighlightFirst || query.length > 0; - if (gl.GfmAutoComplete.isLoading(items)) { - this.setting.highlightFirst = false; - return items; - } - return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey); - }, - filter: function(query, data, searchKey) { - if (gl.GfmAutoComplete.isLoading(data)) { - gl.GfmAutoComplete.fetchData(this.$inputor, this.at); - return data; - } else { - return $.fn.atwho["default"].callbacks.filter(query, data, searchKey); - } - }, - beforeInsert: function(value) { - if (value && !this.setting.skipSpecialCharacterTest) { - var withoutAt = value.substring(1); - if (withoutAt && /[^\w\d]/.test(withoutAt)) value = value.charAt() + '"' + withoutAt + '"'; - } - return value; - }, - matcher: function (flag, subtext) { - // The below is taken from At.js source - // Tweaked to commands to start without a space only if char before is a non-word character - // https://github.com/ichord/At.js - var _a, _y, regexp, match, atSymbolsWithBar, atSymbolsWithoutBar; - atSymbolsWithBar = Object.keys(this.app.controllers).join('|'); - atSymbolsWithoutBar = Object.keys(this.app.controllers).join(''); - subtext = subtext.split(/\s+/g).pop(); - flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + matcher: function (flag, subtext) { + // The below is taken from At.js source + // Tweaked to commands to start without a space only if char before is a non-word character + // https://github.com/ichord/At.js + var _a, _y, regexp, match, atSymbolsWithBar, atSymbolsWithoutBar; + atSymbolsWithBar = Object.keys(this.app.controllers).join('|'); + atSymbolsWithoutBar = Object.keys(this.app.controllers).join(''); + subtext = subtext.split(/\s+/g).pop(); + flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); - _a = decodeURI("%C3%80"); - _y = decodeURI("%C3%BF"); + _a = decodeURI("%C3%80"); + _y = decodeURI("%C3%BF"); - regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?!" + atSymbolsWithBar + ")((?:[A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]|[^\\x00-\\x7a])*)$", 'gi'); + regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?!" + atSymbolsWithBar + ")((?:[A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]|[^\\x00-\\x7a])*)$", 'gi'); - match = regexp.exec(subtext); + match = regexp.exec(subtext); - if (match) { - return match[1]; - } else { - return null; - } + if (match) { + return match[1]; + } else { + return null; } - }, - setup: function(input) { - // Add GFM auto-completion to all input fields, that accept GFM input. - this.input = input || $('.js-gfm-input'); - this.setupLifecycle(); - }, - setupLifecycle() { - this.input.each((i, input) => { - const $input = $(input); - $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input)); - // This triggers at.js again - // Needed for slash commands with suffixes (ex: /label ~) - $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup')); - }); - }, - setupAtWho: function($input) { - // Emoji - $input.atwho({ - at: ':', - displayTpl: function(value) { - return value && value.name ? this.Emoji.templateFunction(value.name) : this.Loading.template; - }.bind(this), - insertTpl: ':${name}:', - skipSpecialCharacterTest: true, - data: this.defaultLoadingData, - callbacks: { - sorter: this.DefaultOptions.sorter, - beforeInsert: this.DefaultOptions.beforeInsert, - filter: this.DefaultOptions.filter - } - }); - // Team Members - $input.atwho({ - at: '@', - displayTpl: function(value) { - return value.username != null ? this.Members.template : this.Loading.template; - }.bind(this), - insertTpl: '${atwho-at}${username}', - searchKey: 'search', - alwaysHighlightFirst: true, - skipSpecialCharacterTest: true, - data: this.defaultLoadingData, - callbacks: { - sorter: this.DefaultOptions.sorter, - filter: this.DefaultOptions.filter, - beforeInsert: this.DefaultOptions.beforeInsert, - matcher: this.DefaultOptions.matcher, - beforeSave: function(members) { - return $.map(members, function(m) { - let title = ''; - if (m.username == null) { - return m; - } - title = m.name; - if (m.count) { - title += " (" + m.count + ")"; - } + } + }, + setup: function(input) { + // Add GFM auto-completion to all input fields, that accept GFM input. + this.input = input || $('.js-gfm-input'); + this.setupLifecycle(); + }, + setupLifecycle() { + this.input.each((i, input) => { + const $input = $(input); + $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input)); + // This triggers at.js again + // Needed for slash commands with suffixes (ex: /label ~) + $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup')); + }); + }, + setupAtWho: function($input) { + // Emoji + $input.atwho({ + at: ':', + displayTpl: function(value) { + return value && value.name ? this.Emoji.templateFunction(value.name) : this.Loading.template; + }.bind(this), + insertTpl: ':${name}:', + skipSpecialCharacterTest: true, + data: this.defaultLoadingData, + callbacks: { + sorter: this.DefaultOptions.sorter, + beforeInsert: this.DefaultOptions.beforeInsert, + filter: this.DefaultOptions.filter + } + }); + // Team Members + $input.atwho({ + at: '@', + displayTpl: function(value) { + return value.username != null ? this.Members.template : this.Loading.template; + }.bind(this), + insertTpl: '${atwho-at}${username}', + searchKey: 'search', + alwaysHighlightFirst: true, + skipSpecialCharacterTest: true, + data: this.defaultLoadingData, + callbacks: { + sorter: this.DefaultOptions.sorter, + filter: this.DefaultOptions.filter, + beforeInsert: this.DefaultOptions.beforeInsert, + matcher: this.DefaultOptions.matcher, + beforeSave: function(members) { + return $.map(members, function(m) { + let title = ''; + if (m.username == null) { + return m; + } + title = m.name; + if (m.count) { + title += " (" + m.count + ")"; + } - const autoCompleteAvatar = m.avatar_url || m.username.charAt(0).toUpperCase(); - const imgAvatar = `${m.username}`; - const txtAvatar = `
      ${autoCompleteAvatar}
      `; + const autoCompleteAvatar = m.avatar_url || m.username.charAt(0).toUpperCase(); + const imgAvatar = `${m.username}`; + const txtAvatar = `
      ${autoCompleteAvatar}
      `; - return { - username: m.username, - avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar, - title: sanitize(title), - search: sanitize(m.username + " " + m.name) - }; - }); - } + return { + username: m.username, + avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar, + title: sanitize(title), + search: sanitize(m.username + " " + m.name) + }; + }); } - }); - $input.atwho({ - at: '#', - alias: 'issues', - searchKey: 'search', - displayTpl: function(value) { - return value.title != null ? this.Issues.template : this.Loading.template; - }.bind(this), - data: this.defaultLoadingData, - insertTpl: '${atwho-at}${id}', - callbacks: { - sorter: this.DefaultOptions.sorter, - filter: this.DefaultOptions.filter, - beforeInsert: this.DefaultOptions.beforeInsert, - matcher: this.DefaultOptions.matcher, - beforeSave: function(issues) { - return $.map(issues, function(i) { - if (i.title == null) { - return i; - } - return { - id: i.iid, - title: sanitize(i.title), - search: i.iid + " " + i.title - }; - }); - } + } + }); + $input.atwho({ + at: '#', + alias: 'issues', + searchKey: 'search', + displayTpl: function(value) { + return value.title != null ? this.Issues.template : this.Loading.template; + }.bind(this), + data: this.defaultLoadingData, + insertTpl: '${atwho-at}${id}', + callbacks: { + sorter: this.DefaultOptions.sorter, + filter: this.DefaultOptions.filter, + beforeInsert: this.DefaultOptions.beforeInsert, + matcher: this.DefaultOptions.matcher, + beforeSave: function(issues) { + return $.map(issues, function(i) { + if (i.title == null) { + return i; + } + return { + id: i.iid, + title: sanitize(i.title), + search: i.iid + " " + i.title + }; + }); } - }); - $input.atwho({ - at: '%', - alias: 'milestones', - searchKey: 'search', - insertTpl: '${atwho-at}${title}', - displayTpl: function(value) { - return value.title != null ? this.Milestones.template : this.Loading.template; - }.bind(this), - data: this.defaultLoadingData, - callbacks: { - matcher: this.DefaultOptions.matcher, - sorter: this.DefaultOptions.sorter, - beforeInsert: this.DefaultOptions.beforeInsert, - filter: this.DefaultOptions.filter, - beforeSave: function(milestones) { - return $.map(milestones, function(m) { - if (m.title == null) { - return m; - } - return { - id: m.iid, - title: sanitize(m.title), - search: "" + m.title - }; - }); - } + } + }); + $input.atwho({ + at: '%', + alias: 'milestones', + searchKey: 'search', + insertTpl: '${atwho-at}${title}', + displayTpl: function(value) { + return value.title != null ? this.Milestones.template : this.Loading.template; + }.bind(this), + data: this.defaultLoadingData, + callbacks: { + matcher: this.DefaultOptions.matcher, + sorter: this.DefaultOptions.sorter, + beforeInsert: this.DefaultOptions.beforeInsert, + filter: this.DefaultOptions.filter, + beforeSave: function(milestones) { + return $.map(milestones, function(m) { + if (m.title == null) { + return m; + } + return { + id: m.iid, + title: sanitize(m.title), + search: "" + m.title + }; + }); } - }); - $input.atwho({ - at: '!', - alias: 'mergerequests', - searchKey: 'search', - displayTpl: function(value) { - return value.title != null ? this.Issues.template : this.Loading.template; - }.bind(this), - data: this.defaultLoadingData, - insertTpl: '${atwho-at}${id}', - callbacks: { - sorter: this.DefaultOptions.sorter, - filter: this.DefaultOptions.filter, - beforeInsert: this.DefaultOptions.beforeInsert, - matcher: this.DefaultOptions.matcher, - beforeSave: function(merges) { - return $.map(merges, function(m) { - if (m.title == null) { - return m; - } - return { - id: m.iid, - title: sanitize(m.title), - search: m.iid + " " + m.title - }; - }); - } + } + }); + $input.atwho({ + at: '!', + alias: 'mergerequests', + searchKey: 'search', + displayTpl: function(value) { + return value.title != null ? this.Issues.template : this.Loading.template; + }.bind(this), + data: this.defaultLoadingData, + insertTpl: '${atwho-at}${id}', + callbacks: { + sorter: this.DefaultOptions.sorter, + filter: this.DefaultOptions.filter, + beforeInsert: this.DefaultOptions.beforeInsert, + matcher: this.DefaultOptions.matcher, + beforeSave: function(merges) { + return $.map(merges, function(m) { + if (m.title == null) { + return m; + } + return { + id: m.iid, + title: sanitize(m.title), + search: m.iid + " " + m.title + }; + }); } - }); - $input.atwho({ - at: '~', - alias: 'labels', - searchKey: 'search', - data: this.defaultLoadingData, - displayTpl: function(value) { - return this.isLoading(value) ? this.Loading.template : this.Labels.template; - }.bind(this), - insertTpl: '${atwho-at}${title}', - callbacks: { - matcher: this.DefaultOptions.matcher, - beforeInsert: this.DefaultOptions.beforeInsert, - filter: this.DefaultOptions.filter, - sorter: this.DefaultOptions.sorter, - beforeSave: function(merges) { - if (gl.GfmAutoComplete.isLoading(merges)) return merges; - var sanitizeLabelTitle; - sanitizeLabelTitle = function(title) { - if (/[\w\?&]+\s+[\w\?&]+/g.test(title)) { - return "\"" + (sanitize(title)) + "\""; - } else { - return sanitize(title); - } + } + }); + $input.atwho({ + at: '~', + alias: 'labels', + searchKey: 'search', + data: this.defaultLoadingData, + displayTpl: function(value) { + return this.isLoading(value) ? this.Loading.template : this.Labels.template; + }.bind(this), + insertTpl: '${atwho-at}${title}', + callbacks: { + matcher: this.DefaultOptions.matcher, + beforeInsert: this.DefaultOptions.beforeInsert, + filter: this.DefaultOptions.filter, + sorter: this.DefaultOptions.sorter, + beforeSave: function(merges) { + if (gl.GfmAutoComplete.isLoading(merges)) return merges; + var sanitizeLabelTitle; + sanitizeLabelTitle = function(title) { + if (/[\w\?&]+\s+[\w\?&]+/g.test(title)) { + return "\"" + (sanitize(title)) + "\""; + } else { + return sanitize(title); + } + }; + return $.map(merges, function(m) { + return { + title: sanitize(m.title), + color: m.color, + search: "" + m.title }; - return $.map(merges, function(m) { - return { - title: sanitize(m.title), - color: m.color, - search: "" + m.title - }; - }); - } + }); } - }); - // We don't instantiate the slash commands autocomplete for note and issue/MR edit forms - $input.filter('[data-supports-slash-commands="true"]').atwho({ - at: '/', - alias: 'commands', - searchKey: 'search', - skipSpecialCharacterTest: true, - data: this.defaultLoadingData, - displayTpl: function(value) { - if (this.isLoading(value)) return this.Loading.template; - var tpl = '
    • /${name}'; - if (value.aliases.length > 0) { - tpl += ' (or /<%- aliases.join(", /") %>)'; - } - if (value.params.length > 0) { - tpl += ' <%- params.join(" ") %>'; - } - if (value.description !== '') { - tpl += '<%- description %>'; + } + }); + // We don't instantiate the slash commands autocomplete for note and issue/MR edit forms + $input.filter('[data-supports-slash-commands="true"]').atwho({ + at: '/', + alias: 'commands', + searchKey: 'search', + skipSpecialCharacterTest: true, + data: this.defaultLoadingData, + displayTpl: function(value) { + if (this.isLoading(value)) return this.Loading.template; + var tpl = '
    • /${name}'; + if (value.aliases.length > 0) { + tpl += ' (or /<%- aliases.join(", /") %>)'; + } + if (value.params.length > 0) { + tpl += ' <%- params.join(" ") %>'; + } + if (value.description !== '') { + tpl += '<%- description %>'; + } + tpl += '
    • '; + return _.template(tpl)(value); + }.bind(this), + insertTpl: function(value) { + var tpl = "/${name} "; + var reference_prefix = null; + if (value.params.length > 0) { + reference_prefix = value.params[0][0]; + if (/^[@%~]/.test(reference_prefix)) { + tpl += '<%- reference_prefix %>'; } - tpl += ''; - return _.template(tpl)(value); - }.bind(this), - insertTpl: function(value) { - var tpl = "/${name} "; - var reference_prefix = null; - if (value.params.length > 0) { - reference_prefix = value.params[0][0]; - if (/^[@%~]/.test(reference_prefix)) { - tpl += '<%- reference_prefix %>'; + } + return _.template(tpl)({ reference_prefix: reference_prefix }); + }, + suffix: '', + callbacks: { + sorter: this.DefaultOptions.sorter, + filter: this.DefaultOptions.filter, + beforeInsert: this.DefaultOptions.beforeInsert, + beforeSave: function(commands) { + if (gl.GfmAutoComplete.isLoading(commands)) return commands; + return $.map(commands, function(c) { + var search = c.name; + if (c.aliases.length > 0) { + search = search + " " + c.aliases.join(" "); } - } - return _.template(tpl)({ reference_prefix: reference_prefix }); + return { + name: c.name, + aliases: c.aliases, + params: c.params, + description: c.description, + search: search + }; + }); }, - suffix: '', - callbacks: { - sorter: this.DefaultOptions.sorter, - filter: this.DefaultOptions.filter, - beforeInsert: this.DefaultOptions.beforeInsert, - beforeSave: function(commands) { - if (gl.GfmAutoComplete.isLoading(commands)) return commands; - return $.map(commands, function(c) { - var search = c.name; - if (c.aliases.length > 0) { - search = search + " " + c.aliases.join(" "); - } - return { - name: c.name, - aliases: c.aliases, - params: c.params, - description: c.description, - search: search - }; - }); - }, - matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) { - var regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi; - var match = regexp.exec(subtext); - if (match) { - return match[1]; - } else { - return null; - } + matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) { + var regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi; + var match = regexp.exec(subtext); + if (match) { + return match[1]; + } else { + return null; } } - }); - return; - }, - fetchData: function($input, at) { - if (this.isLoadingData[at]) return; - this.isLoadingData[at] = true; - if (this.cachedData[at]) { - this.loadData($input, at, this.cachedData[at]); - } else if (this.atTypeMap[at] === 'emojis') { - this.loadData($input, at, Object.keys(emojiMap).concat(Object.keys(emojiAliases))); - } else { - $.getJSON(this.dataSources[this.atTypeMap[at]], (data) => { - this.loadData($input, at, data); - }).fail(() => { this.isLoadingData[at] = false; }); - } - }, - loadData: function($input, at, data) { - this.isLoadingData[at] = false; - this.cachedData[at] = data; - $input.atwho('load', at, data); - // This trigger at.js again - // otherwise we would be stuck with loading until the user types - return $input.trigger('keyup'); - }, - isLoading(data) { - var dataToInspect = data; - if (data && data.length > 0) { - dataToInspect = data[0]; } - - var loadingState = this.defaultLoadingData[0]; - return dataToInspect && - (dataToInspect === loadingState || dataToInspect.name === loadingState); + }); + return; + }, + fetchData: function($input, at) { + if (this.isLoadingData[at]) return; + this.isLoadingData[at] = true; + if (this.cachedData[at]) { + this.loadData($input, at, this.cachedData[at]); + } else if (this.atTypeMap[at] === 'emojis') { + this.loadData($input, at, Object.keys(emojiMap).concat(Object.keys(emojiAliases))); + } else { + $.getJSON(this.dataSources[this.atTypeMap[at]], (data) => { + this.loadData($input, at, data); + }).fail(() => { this.isLoadingData[at] = false; }); + } + }, + loadData: function($input, at, data) { + this.isLoadingData[at] = false; + this.cachedData[at] = data; + $input.atwho('load', at, data); + // This trigger at.js again + // otherwise we would be stuck with loading until the user types + return $input.trigger('keyup'); + }, + isLoading(data) { + var dataToInspect = data; + if (data && data.length > 0) { + dataToInspect = data[0]; } - }; -}).call(window); + + var loadingState = this.defaultLoadingData[0]; + return dataToInspect && + (dataToInspect === loadingState || dataToInspect.name === loadingState); + } +}; diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 9e6ed06054b..a03f1202a6d 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -1,850 +1,848 @@ /* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, prefer-rest-params, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func, no-mixed-operators */ /* global fuzzaldrinPlus */ -(function() { - var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, - bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }, - indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; }; - - GitLabDropdownFilter = (function() { - var ARROW_KEY_CODES, BLUR_KEYCODES, HAS_VALUE_CLASS; - - BLUR_KEYCODES = [27, 40]; - - ARROW_KEY_CODES = [38, 40]; - - HAS_VALUE_CLASS = "has-value"; - - function GitLabDropdownFilter(input, options) { - var $clearButton, $inputContainer, ref, timeout; - this.input = input; - this.options = options; - this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true; - $inputContainer = this.input.parent(); - $clearButton = $inputContainer.find('.js-dropdown-input-clear'); - $clearButton.on('click', (function(_this) { - // Clear click - return function(e) { +var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, + bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }, + indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; }; + +GitLabDropdownFilter = (function() { + var ARROW_KEY_CODES, BLUR_KEYCODES, HAS_VALUE_CLASS; + + BLUR_KEYCODES = [27, 40]; + + ARROW_KEY_CODES = [38, 40]; + + HAS_VALUE_CLASS = "has-value"; + + function GitLabDropdownFilter(input, options) { + var $clearButton, $inputContainer, ref, timeout; + this.input = input; + this.options = options; + this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true; + $inputContainer = this.input.parent(); + $clearButton = $inputContainer.find('.js-dropdown-input-clear'); + $clearButton.on('click', (function(_this) { + // Clear click + return function(e) { + e.preventDefault(); + e.stopPropagation(); + return _this.input.val('').trigger('input').focus(); + }; + })(this)); + // Key events + timeout = ""; + this.input + .on('keydown', function (e) { + var keyCode = e.which; + if (keyCode === 13 && !options.elIsInput) { e.preventDefault(); - e.stopPropagation(); - return _this.input.val('').trigger('input').focus(); - }; - })(this)); - // Key events - timeout = ""; - this.input - .on('keydown', function (e) { - var keyCode = e.which; - if (keyCode === 13 && !options.elIsInput) { - e.preventDefault(); - } - }) - .on('input', function() { - if (this.input.val() !== "" && !$inputContainer.hasClass(HAS_VALUE_CLASS)) { - $inputContainer.addClass(HAS_VALUE_CLASS); - } else if (this.input.val() === "" && $inputContainer.hasClass(HAS_VALUE_CLASS)) { - $inputContainer.removeClass(HAS_VALUE_CLASS); - } - // Only filter asynchronously only if option remote is set - if (this.options.remote) { - clearTimeout(timeout); - return timeout = setTimeout(function() { - $inputContainer.parent().addClass('is-loading'); - - return this.options.query(this.input.val(), function(data) { - $inputContainer.parent().removeClass('is-loading'); - return this.options.callback(data); - }.bind(this)); - }.bind(this), 250); - } else { - return this.filter(this.input.val()); - } - }.bind(this)); - } + } + }) + .on('input', function() { + if (this.input.val() !== "" && !$inputContainer.hasClass(HAS_VALUE_CLASS)) { + $inputContainer.addClass(HAS_VALUE_CLASS); + } else if (this.input.val() === "" && $inputContainer.hasClass(HAS_VALUE_CLASS)) { + $inputContainer.removeClass(HAS_VALUE_CLASS); + } + // Only filter asynchronously only if option remote is set + if (this.options.remote) { + clearTimeout(timeout); + return timeout = setTimeout(function() { + $inputContainer.parent().addClass('is-loading'); + + return this.options.query(this.input.val(), function(data) { + $inputContainer.parent().removeClass('is-loading'); + return this.options.callback(data); + }.bind(this)); + }.bind(this), 250); + } else { + return this.filter(this.input.val()); + } + }.bind(this)); + } - GitLabDropdownFilter.prototype.shouldBlur = function(keyCode) { - return BLUR_KEYCODES.indexOf(keyCode) !== -1; - }; + GitLabDropdownFilter.prototype.shouldBlur = function(keyCode) { + return BLUR_KEYCODES.indexOf(keyCode) !== -1; + }; - GitLabDropdownFilter.prototype.filter = function(search_text) { - var data, elements, group, key, results, tmp; - if (this.options.onFilter) { - this.options.onFilter(search_text); - } - data = this.options.data(); - if ((data != null) && !this.options.filterByText) { - results = data; - if (search_text !== '') { - // When data is an array of objects therefore [object Array] e.g. - // [ - // { prop: 'foo' }, - // { prop: 'baz' } - // ] - if (_.isArray(data)) { - results = fuzzaldrinPlus.filter(data, search_text, { - key: this.options.keys - }); - } else { - // If data is grouped therefore an [object Object]. e.g. - // { - // groupName1: [ - // { prop: 'foo' }, - // { prop: 'baz' } - // ], - // groupName2: [ - // { prop: 'abc' }, - // { prop: 'def' } - // ] - // } - if (gl.utils.isObject(data)) { - results = {}; - for (key in data) { - group = data[key]; - tmp = fuzzaldrinPlus.filter(group, search_text, { - key: this.options.keys + GitLabDropdownFilter.prototype.filter = function(search_text) { + var data, elements, group, key, results, tmp; + if (this.options.onFilter) { + this.options.onFilter(search_text); + } + data = this.options.data(); + if ((data != null) && !this.options.filterByText) { + results = data; + if (search_text !== '') { + // When data is an array of objects therefore [object Array] e.g. + // [ + // { prop: 'foo' }, + // { prop: 'baz' } + // ] + if (_.isArray(data)) { + results = fuzzaldrinPlus.filter(data, search_text, { + key: this.options.keys + }); + } else { + // If data is grouped therefore an [object Object]. e.g. + // { + // groupName1: [ + // { prop: 'foo' }, + // { prop: 'baz' } + // ], + // groupName2: [ + // { prop: 'abc' }, + // { prop: 'def' } + // ] + // } + if (gl.utils.isObject(data)) { + results = {}; + for (key in data) { + group = data[key]; + tmp = fuzzaldrinPlus.filter(group, search_text, { + key: this.options.keys + }); + if (tmp.length) { + results[key] = tmp.map(function(item) { + return item; }); - if (tmp.length) { - results[key] = tmp.map(function(item) { - return item; - }); - } } } } } - return this.options.callback(results); - } else { - elements = this.options.elements(); - if (search_text) { - return elements.each(function() { - var $el, matches; - $el = $(this); - matches = fuzzaldrinPlus.match($el.text().trim(), search_text); - if (!$el.is('.dropdown-header')) { - if (matches.length) { - return $el.show().removeClass('option-hidden'); - } else { - return $el.hide().addClass('option-hidden'); - } + } + return this.options.callback(results); + } else { + elements = this.options.elements(); + if (search_text) { + return elements.each(function() { + var $el, matches; + $el = $(this); + matches = fuzzaldrinPlus.match($el.text().trim(), search_text); + if (!$el.is('.dropdown-header')) { + if (matches.length) { + return $el.show().removeClass('option-hidden'); + } else { + return $el.hide().addClass('option-hidden'); } - }); - } else { - return elements.show().removeClass('option-hidden'); - } + } + }); + } else { + return elements.show().removeClass('option-hidden'); } - }; - - return GitLabDropdownFilter; - })(); + } + }; - GitLabDropdownRemote = (function() { - function GitLabDropdownRemote(dataEndpoint, options) { - this.dataEndpoint = dataEndpoint; - this.options = options; + return GitLabDropdownFilter; +})(); + +GitLabDropdownRemote = (function() { + function GitLabDropdownRemote(dataEndpoint, options) { + this.dataEndpoint = dataEndpoint; + this.options = options; + } + + GitLabDropdownRemote.prototype.execute = function() { + if (typeof this.dataEndpoint === "string") { + return this.fetchData(); + } else if (typeof this.dataEndpoint === "function") { + if (this.options.beforeSend) { + this.options.beforeSend(); + } + return this.dataEndpoint("", (function(_this) { + // Fetch the data by calling the data funcfion + return function(data) { + if (_this.options.success) { + _this.options.success(data); + } + if (_this.options.beforeSend) { + return _this.options.beforeSend(); + } + }; + })(this)); } + }; - GitLabDropdownRemote.prototype.execute = function() { - if (typeof this.dataEndpoint === "string") { - return this.fetchData(); - } else if (typeof this.dataEndpoint === "function") { - if (this.options.beforeSend) { - this.options.beforeSend(); - } - return this.dataEndpoint("", (function(_this) { - // Fetch the data by calling the data funcfion - return function(data) { - if (_this.options.success) { - _this.options.success(data); - } - if (_this.options.beforeSend) { - return _this.options.beforeSend(); - } - }; - })(this)); - } - }; + GitLabDropdownRemote.prototype.fetchData = function() { + return $.ajax({ + url: this.dataEndpoint, + dataType: this.options.dataType, + beforeSend: (function(_this) { + return function() { + if (_this.options.beforeSend) { + return _this.options.beforeSend(); + } + }; + })(this), + success: (function(_this) { + return function(data) { + if (_this.options.success) { + return _this.options.success(data); + } + }; + })(this) + }); + // Fetch the data through ajax if the data is a string + }; - GitLabDropdownRemote.prototype.fetchData = function() { - return $.ajax({ - url: this.dataEndpoint, - dataType: this.options.dataType, - beforeSend: (function(_this) { - return function() { - if (_this.options.beforeSend) { - return _this.options.beforeSend(); - } - }; - })(this), - success: (function(_this) { - return function(data) { - if (_this.options.success) { - return _this.options.success(data); - } - }; - })(this) - }); - // Fetch the data through ajax if the data is a string - }; + return GitLabDropdownRemote; +})(); - return GitLabDropdownRemote; - })(); +GitLabDropdown = (function() { + var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, CURSOR_SELECT_SCROLL_PADDING, currentIndex; - GitLabDropdown = (function() { - var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, CURSOR_SELECT_SCROLL_PADDING, currentIndex; + LOADING_CLASS = "is-loading"; - LOADING_CLASS = "is-loading"; + PAGE_TWO_CLASS = "is-page-two"; - PAGE_TWO_CLASS = "is-page-two"; + ACTIVE_CLASS = "is-active"; - ACTIVE_CLASS = "is-active"; + INDETERMINATE_CLASS = "is-indeterminate"; - INDETERMINATE_CLASS = "is-indeterminate"; + currentIndex = -1; - currentIndex = -1; + NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link'; - NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link'; - - SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ", .option-hidden)"; - - CURSOR_SELECT_SCROLL_PADDING = 5; - - FILTER_INPUT = '.dropdown-input .dropdown-input-field'; - - function GitLabDropdown(el1, options) { - var searchFields, selector, self; - this.el = el1; - this.options = options; - this.updateLabel = bind(this.updateLabel, this); - this.hidden = bind(this.hidden, this); - this.opened = bind(this.opened, this); - this.shouldPropagate = bind(this.shouldPropagate, this); - self = this; - selector = $(this.el).data("target"); - this.dropdown = selector != null ? $(selector) : $(this.el).parent(); - // Set Defaults - this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT); - this.highlight = !!this.options.highlight; - this.filterInputBlur = this.options.filterInputBlur != null - ? this.options.filterInputBlur - : true; - // If no input is passed create a default one - self = this; - // If selector was passed - if (_.isString(this.filterInput)) { - this.filterInput = this.getElement(this.filterInput); - } - searchFields = this.options.search ? this.options.search.fields : []; - if (this.options.data) { - // If we provided data - // data could be an array of objects or a group of arrays - if (_.isObject(this.options.data) && !_.isFunction(this.options.data)) { - this.fullData = this.options.data; - currentIndex = -1; - this.parseData(this.options.data); - this.focusTextInput(); - } else { - this.remote = new GitLabDropdownRemote(this.options.data, { - dataType: this.options.dataType, - beforeSend: this.toggleLoading.bind(this), - success: (function(_this) { - return function(data) { - _this.fullData = data; - _this.parseData(_this.fullData); - _this.focusTextInput(); - if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val() && _this.filter.input.val().trim() !== '') { - return _this.filter.input.trigger('input'); - } - }; - // Remote data - })(this) - }); - } - } - // Init filterable - if (this.options.filterable) { - this.filter = new GitLabDropdownFilter(this.filterInput, { - elIsInput: $(this.el).is('input'), - filterInputBlur: this.filterInputBlur, - filterByText: this.options.filterByText, - onFilter: this.options.onFilter, - remote: this.options.filterRemote, - query: this.options.data, - keys: searchFields, - elements: (function(_this) { - return function() { - selector = '.dropdown-content li:not(' + NON_SELECTABLE_CLASSES + ')'; - if (_this.dropdown.find('.dropdown-toggle-page').length) { - selector = ".dropdown-page-one " + selector; - } - return $(selector); - }; - })(this), - data: (function(_this) { - return function() { - return _this.fullData; - }; - })(this), - callback: (function(_this) { + SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ", .option-hidden)"; + + CURSOR_SELECT_SCROLL_PADDING = 5; + + FILTER_INPUT = '.dropdown-input .dropdown-input-field'; + + function GitLabDropdown(el1, options) { + var searchFields, selector, self; + this.el = el1; + this.options = options; + this.updateLabel = bind(this.updateLabel, this); + this.hidden = bind(this.hidden, this); + this.opened = bind(this.opened, this); + this.shouldPropagate = bind(this.shouldPropagate, this); + self = this; + selector = $(this.el).data("target"); + this.dropdown = selector != null ? $(selector) : $(this.el).parent(); + // Set Defaults + this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT); + this.highlight = !!this.options.highlight; + this.filterInputBlur = this.options.filterInputBlur != null + ? this.options.filterInputBlur + : true; + // If no input is passed create a default one + self = this; + // If selector was passed + if (_.isString(this.filterInput)) { + this.filterInput = this.getElement(this.filterInput); + } + searchFields = this.options.search ? this.options.search.fields : []; + if (this.options.data) { + // If we provided data + // data could be an array of objects or a group of arrays + if (_.isObject(this.options.data) && !_.isFunction(this.options.data)) { + this.fullData = this.options.data; + currentIndex = -1; + this.parseData(this.options.data); + this.focusTextInput(); + } else { + this.remote = new GitLabDropdownRemote(this.options.data, { + dataType: this.options.dataType, + beforeSend: this.toggleLoading.bind(this), + success: (function(_this) { return function(data) { - _this.parseData(data); - if (_this.filterInput.val() !== '') { - selector = SELECTABLE_CLASSES; - if (_this.dropdown.find('.dropdown-toggle-page').length) { - selector = ".dropdown-page-one " + selector; - } - if ($(_this.el).is('input')) { - currentIndex = -1; - } else { - $(selector, _this.dropdown).first().find('a').addClass('is-focused'); - currentIndex = 0; - } + _this.fullData = data; + _this.parseData(_this.fullData); + _this.focusTextInput(); + if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val() && _this.filter.input.val().trim() !== '') { + return _this.filter.input.trigger('input'); } }; + // Remote data })(this) }); } - // Event listeners - this.dropdown.on("shown.bs.dropdown", this.opened); - this.dropdown.on("hidden.bs.dropdown", this.hidden); - $(this.el).on("update.label", this.updateLabel); - this.dropdown.on("click", ".dropdown-menu, .dropdown-menu-close", this.shouldPropagate); - this.dropdown.on('keyup', (function(_this) { - return function(e) { - // Escape key - if (e.which === 27) { - return $('.dropdown-menu-close', _this.dropdown).trigger('click'); - } - }; - })(this)); - this.dropdown.on('blur', 'a', (function(_this) { - return function(e) { - var $dropdownMenu, $relatedTarget; - if (e.relatedTarget != null) { - $relatedTarget = $(e.relatedTarget); - $dropdownMenu = $relatedTarget.closest('.dropdown-menu'); - if ($dropdownMenu.length === 0) { - return _this.dropdown.removeClass('open'); + } + // Init filterable + if (this.options.filterable) { + this.filter = new GitLabDropdownFilter(this.filterInput, { + elIsInput: $(this.el).is('input'), + filterInputBlur: this.filterInputBlur, + filterByText: this.options.filterByText, + onFilter: this.options.onFilter, + remote: this.options.filterRemote, + query: this.options.data, + keys: searchFields, + elements: (function(_this) { + return function() { + selector = '.dropdown-content li:not(' + NON_SELECTABLE_CLASSES + ')'; + if (_this.dropdown.find('.dropdown-toggle-page').length) { + selector = ".dropdown-page-one " + selector; + } + return $(selector); + }; + })(this), + data: (function(_this) { + return function() { + return _this.fullData; + }; + })(this), + callback: (function(_this) { + return function(data) { + _this.parseData(data); + if (_this.filterInput.val() !== '') { + selector = SELECTABLE_CLASSES; + if (_this.dropdown.find('.dropdown-toggle-page').length) { + selector = ".dropdown-page-one " + selector; + } + if ($(_this.el).is('input')) { + currentIndex = -1; + } else { + $(selector, _this.dropdown).first().find('a').addClass('is-focused'); + currentIndex = 0; + } } + }; + })(this) + }); + } + // Event listeners + this.dropdown.on("shown.bs.dropdown", this.opened); + this.dropdown.on("hidden.bs.dropdown", this.hidden); + $(this.el).on("update.label", this.updateLabel); + this.dropdown.on("click", ".dropdown-menu, .dropdown-menu-close", this.shouldPropagate); + this.dropdown.on('keyup', (function(_this) { + return function(e) { + // Escape key + if (e.which === 27) { + return $('.dropdown-menu-close', _this.dropdown).trigger('click'); + } + }; + })(this)); + this.dropdown.on('blur', 'a', (function(_this) { + return function(e) { + var $dropdownMenu, $relatedTarget; + if (e.relatedTarget != null) { + $relatedTarget = $(e.relatedTarget); + $dropdownMenu = $relatedTarget.closest('.dropdown-menu'); + if ($dropdownMenu.length === 0) { + return _this.dropdown.removeClass('open'); } + } + }; + })(this)); + if (this.dropdown.find(".dropdown-toggle-page").length) { + this.dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on("click", (function(_this) { + return function(e) { + e.preventDefault(); + e.stopPropagation(); + return _this.togglePage(); }; })(this)); + } + if (this.options.selectable) { + selector = ".dropdown-content a"; if (this.dropdown.find(".dropdown-toggle-page").length) { - this.dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on("click", (function(_this) { - return function(e) { - e.preventDefault(); - e.stopPropagation(); - return _this.togglePage(); - }; - })(this)); - } - if (this.options.selectable) { - selector = ".dropdown-content a"; - if (this.dropdown.find(".dropdown-toggle-page").length) { - selector = ".dropdown-page-one .dropdown-content a"; + selector = ".dropdown-page-one .dropdown-content a"; + } + this.dropdown.on("click", selector, function(e) { + var $el, selected, selectedObj, isMarking; + $el = $(this); + selected = self.rowClicked($el); + selectedObj = selected ? selected[0] : null; + isMarking = selected ? selected[1] : null; + if (self.options.clicked) { + self.options.clicked(selectedObj, $el, e, isMarking); } - this.dropdown.on("click", selector, function(e) { - var $el, selected, selectedObj, isMarking; - $el = $(this); - selected = self.rowClicked($el); - selectedObj = selected ? selected[0] : null; - isMarking = selected ? selected[1] : null; - if (self.options.clicked) { - self.options.clicked(selectedObj, $el, e, isMarking); - } - // Update label right after all modifications in dropdown has been done - if (self.options.toggleLabel) { - self.updateLabel(selectedObj, $el, self); - } + // Update label right after all modifications in dropdown has been done + if (self.options.toggleLabel) { + self.updateLabel(selectedObj, $el, self); + } - $el.trigger('blur'); - }); - } + $el.trigger('blur'); + }); } + } - // Finds an element inside wrapper element - GitLabDropdown.prototype.getElement = function(selector) { - return this.dropdown.find(selector); - }; + // Finds an element inside wrapper element + GitLabDropdown.prototype.getElement = function(selector) { + return this.dropdown.find(selector); + }; - GitLabDropdown.prototype.toggleLoading = function() { - return $('.dropdown-menu', this.dropdown).toggleClass(LOADING_CLASS); - }; + GitLabDropdown.prototype.toggleLoading = function() { + return $('.dropdown-menu', this.dropdown).toggleClass(LOADING_CLASS); + }; - GitLabDropdown.prototype.togglePage = function() { - var menu; - menu = $('.dropdown-menu', this.dropdown); - if (menu.hasClass(PAGE_TWO_CLASS)) { - if (this.remote) { - this.remote.execute(); - } - } - menu.toggleClass(PAGE_TWO_CLASS); - // Focus first visible input on active page - return this.dropdown.find('[class^="dropdown-page-"]:visible :text:visible:first').focus(); - }; - - GitLabDropdown.prototype.parseData = function(data) { - var full_html, groupData, html, name; - this.renderedData = data; - if (this.options.filterable && data.length === 0) { - // render no matching results - html = [this.noResults()]; - } else { - // Handle array groups - if (gl.utils.isObject(data)) { - html = []; - for (name in data) { - groupData = data[name]; - html.push(this.renderItem({ - header: name - // Add header for each group - }, name)); - this.renderData(groupData, name).map(function(item) { - return html.push(item); - }); - } - } else { - // Render each row - html = this.renderData(data); - } - } - // Render the full menu - full_html = this.renderMenu(html); - return this.appendMenu(full_html); - }; - - GitLabDropdown.prototype.renderData = function(data, group) { - if (group == null) { - group = false; + GitLabDropdown.prototype.togglePage = function() { + var menu; + menu = $('.dropdown-menu', this.dropdown); + if (menu.hasClass(PAGE_TWO_CLASS)) { + if (this.remote) { + this.remote.execute(); } - return data.map((function(_this) { - return function(obj, index) { - return _this.renderItem(obj, group, index); - }; - })(this)); - }; - - GitLabDropdown.prototype.shouldPropagate = function(e) { - var $target; - if (this.options.multiSelect) { - $target = $(e.target); - if ($target && !$target.hasClass('dropdown-menu-close') && - !$target.hasClass('dropdown-menu-close-icon') && - !$target.data('is-link')) { - e.stopPropagation(); - return false; - } else { - return true; + } + menu.toggleClass(PAGE_TWO_CLASS); + // Focus first visible input on active page + return this.dropdown.find('[class^="dropdown-page-"]:visible :text:visible:first').focus(); + }; + + GitLabDropdown.prototype.parseData = function(data) { + var full_html, groupData, html, name; + this.renderedData = data; + if (this.options.filterable && data.length === 0) { + // render no matching results + html = [this.noResults()]; + } else { + // Handle array groups + if (gl.utils.isObject(data)) { + html = []; + for (name in data) { + groupData = data[name]; + html.push(this.renderItem({ + header: name + // Add header for each group + }, name)); + this.renderData(groupData, name).map(function(item) { + return html.push(item); + }); } + } else { + // Render each row + html = this.renderData(data); } - }; + } + // Render the full menu + full_html = this.renderMenu(html); + return this.appendMenu(full_html); + }; - GitLabDropdown.prototype.opened = function(e) { - var contentHtml; - this.resetRows(); - this.addArrowKeyEvent(); + GitLabDropdown.prototype.renderData = function(data, group) { + if (group == null) { + group = false; + } + return data.map((function(_this) { + return function(obj, index) { + return _this.renderItem(obj, group, index); + }; + })(this)); + }; - // Makes indeterminate items effective - if (this.fullData && this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) { - this.parseData(this.fullData); - } - contentHtml = $('.dropdown-content', this.dropdown).html(); - if (this.remote && contentHtml === "") { - this.remote.execute(); + GitLabDropdown.prototype.shouldPropagate = function(e) { + var $target; + if (this.options.multiSelect) { + $target = $(e.target); + if ($target && !$target.hasClass('dropdown-menu-close') && + !$target.hasClass('dropdown-menu-close-icon') && + !$target.data('is-link')) { + e.stopPropagation(); + return false; } else { - this.focusTextInput(); + return true; } + } + }; - if (this.options.showMenuAbove) { - this.positionMenuAbove(); - } + GitLabDropdown.prototype.opened = function(e) { + var contentHtml; + this.resetRows(); + this.addArrowKeyEvent(); - if (this.options.opened) { - this.options.opened.call(this, e); - } + // Makes indeterminate items effective + if (this.fullData && this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) { + this.parseData(this.fullData); + } + contentHtml = $('.dropdown-content', this.dropdown).html(); + if (this.remote && contentHtml === "") { + this.remote.execute(); + } else { + this.focusTextInput(); + } + + if (this.options.showMenuAbove) { + this.positionMenuAbove(); + } - return this.dropdown.trigger('shown.gl.dropdown'); - }; + if (this.options.opened) { + this.options.opened.call(this, e); + } - GitLabDropdown.prototype.positionMenuAbove = function() { - var $button = $(this.el); - var $menu = this.dropdown.find('.dropdown-menu'); + return this.dropdown.trigger('shown.gl.dropdown'); + }; - $menu.css('top', ($button.height() + $menu.height()) * -1); - }; + GitLabDropdown.prototype.positionMenuAbove = function() { + var $button = $(this.el); + var $menu = this.dropdown.find('.dropdown-menu'); - GitLabDropdown.prototype.hidden = function(e) { - var $input; - this.resetRows(); - this.removeArrayKeyEvent(); - $input = this.dropdown.find(".dropdown-input-field"); - if (this.options.filterable) { - $input.blur(); - } - if (this.dropdown.find(".dropdown-toggle-page").length) { - $('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS); - } - if (this.options.hidden) { - this.options.hidden.call(this, e); - } - return this.dropdown.trigger('hidden.gl.dropdown'); - }; + $menu.css('top', ($button.height() + $menu.height()) * -1); + }; - // Render the full menu - GitLabDropdown.prototype.renderMenu = function(html) { - if (this.options.renderMenu) { - return this.options.renderMenu(html); - } else { - var ul = document.createElement('ul'); + GitLabDropdown.prototype.hidden = function(e) { + var $input; + this.resetRows(); + this.removeArrayKeyEvent(); + $input = this.dropdown.find(".dropdown-input-field"); + if (this.options.filterable) { + $input.blur(); + } + if (this.dropdown.find(".dropdown-toggle-page").length) { + $('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS); + } + if (this.options.hidden) { + this.options.hidden.call(this, e); + } + return this.dropdown.trigger('hidden.gl.dropdown'); + }; - for (var i = 0; i < html.length; i += 1) { - var el = html[i]; + // Render the full menu + GitLabDropdown.prototype.renderMenu = function(html) { + if (this.options.renderMenu) { + return this.options.renderMenu(html); + } else { + var ul = document.createElement('ul'); - if (el instanceof jQuery) { - el = el.get(0); - } + for (var i = 0; i < html.length; i += 1) { + var el = html[i]; - if (typeof el === 'string') { - ul.innerHTML += el; - } else { - ul.appendChild(el); - } + if (el instanceof jQuery) { + el = el.get(0); } - return ul; + if (typeof el === 'string') { + ul.innerHTML += el; + } else { + ul.appendChild(el); + } } - }; - // Append the menu into the dropdown - GitLabDropdown.prototype.appendMenu = function(html) { - return this.clearMenu().append(html); - }; + return ul; + } + }; + + // Append the menu into the dropdown + GitLabDropdown.prototype.appendMenu = function(html) { + return this.clearMenu().append(html); + }; - GitLabDropdown.prototype.clearMenu = function() { - var selector; - selector = '.dropdown-content'; - if (this.dropdown.find(".dropdown-toggle-page").length) { - selector = ".dropdown-page-one .dropdown-content"; - } + GitLabDropdown.prototype.clearMenu = function() { + var selector; + selector = '.dropdown-content'; + if (this.dropdown.find(".dropdown-toggle-page").length) { + selector = ".dropdown-page-one .dropdown-content"; + } - return $(selector, this.dropdown).empty(); - }; + return $(selector, this.dropdown).empty(); + }; - GitLabDropdown.prototype.renderItem = function(data, group, index) { - var field, fieldName, html, selected, text, url, value; - if (group == null) { - group = false; + GitLabDropdown.prototype.renderItem = function(data, group, index) { + var field, fieldName, html, selected, text, url, value; + if (group == null) { + group = false; + } + if (index == null) { + // Render the row + index = false; + } + html = document.createElement('li'); + if (data === 'divider' || data === 'separator') { + html.className = data; + return html; + } + // Header + if (data.header != null) { + html.className = 'dropdown-header'; + html.innerHTML = data.header; + return html; + } + if (this.options.renderRow) { + // Call the render function + html = this.options.renderRow.call(this.options, data, this); + } else { + if (!selected) { + value = this.options.id ? this.options.id(data) : data.id; + fieldName = this.options.fieldName; + + if (value) { value = value.toString().replace(/'/g, '\\\''); } + + field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']"); + if (field.length) { + selected = true; + } } - if (index == null) { - // Render the row - index = false; + // Set URL + if (this.options.url != null) { + url = this.options.url(data); + } else { + url = data.url != null ? data.url : '#'; } - html = document.createElement('li'); - if (data === 'divider' || data === 'separator') { - html.className = data; - return html; + // Set Text + if (this.options.text != null) { + text = this.options.text(data); + } else { + text = data.text != null ? data.text : ''; } - // Header - if (data.header != null) { - html.className = 'dropdown-header'; - html.innerHTML = data.header; - return html; + if (this.highlight) { + text = this.highlightTextMatches(text, this.filterInput.val()); } - if (this.options.renderRow) { - // Call the render function - html = this.options.renderRow.call(this.options, data, this); - } else { - if (!selected) { - value = this.options.id ? this.options.id(data) : data.id; - fieldName = this.options.fieldName; - - if (value) { value = value.toString().replace(/'/g, '\\\''); } - - field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']"); - if (field.length) { - selected = true; - } - } - // Set URL - if (this.options.url != null) { - url = this.options.url(data); - } else { - url = data.url != null ? data.url : '#'; - } - // Set Text - if (this.options.text != null) { - text = this.options.text(data); - } else { - text = data.text != null ? data.text : ''; - } - if (this.highlight) { - text = this.highlightTextMatches(text, this.filterInput.val()); - } - // Create the list item & the link - var link = document.createElement('a'); - - link.href = url; - link.innerHTML = text; + // Create the list item & the link + var link = document.createElement('a'); - if (selected) { - link.className = 'is-active'; - } - - if (group) { - link.dataset.group = group; - link.dataset.index = index; - } + link.href = url; + link.innerHTML = text; - html.appendChild(link); + if (selected) { + link.className = 'is-active'; } - return html; - }; - - GitLabDropdown.prototype.highlightTextMatches = function(text, term) { - var occurrences; - occurrences = fuzzaldrinPlus.match(text, term); - return text.split('').map(function(character, i) { - if (indexOf.call(occurrences, i) !== -1) { - return "" + character + ""; - } else { - return character; - } - }).join(''); - }; - - GitLabDropdown.prototype.noResults = function() { - var html; - return html = "
      "; - }; - - GitLabDropdown.prototype.rowClicked = function(el) { - var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value, isMarking; - - fieldName = this.options.fieldName; - isInput = $(this.el).is('input'); - if (this.renderedData) { - groupName = el.data('group'); - if (groupName) { - selectedIndex = el.data('index'); - selectedObject = this.renderedData[groupName][selectedIndex]; - } else { - selectedIndex = el.closest('li').index(); - selectedObject = this.renderedData[selectedIndex]; - } + + if (group) { + link.dataset.group = group; + link.dataset.index = index; } - if (this.options.vue) { - if (el.hasClass(ACTIVE_CLASS)) { - el.removeClass(ACTIVE_CLASS); - } else { - el.addClass(ACTIVE_CLASS); - } + html.appendChild(link); + } + return html; + }; - return [selectedObject]; + GitLabDropdown.prototype.highlightTextMatches = function(text, term) { + var occurrences; + occurrences = fuzzaldrinPlus.match(text, term); + return text.split('').map(function(character, i) { + if (indexOf.call(occurrences, i) !== -1) { + return "" + character + ""; + } else { + return character; } + }).join(''); + }; - field = []; - value = this.options.id - ? this.options.id(selectedObject, el) - : selectedObject.id; - if (isInput) { - field = $(this.el); - } else if (value) { - field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']"); - } + GitLabDropdown.prototype.noResults = function() { + var html; + return html = ""; + }; + + GitLabDropdown.prototype.rowClicked = function(el) { + var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value, isMarking; - if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) { - return; + fieldName = this.options.fieldName; + isInput = $(this.el).is('input'); + if (this.renderedData) { + groupName = el.data('group'); + if (groupName) { + selectedIndex = el.data('index'); + selectedObject = this.renderedData[groupName][selectedIndex]; + } else { + selectedIndex = el.closest('li').index(); + selectedObject = this.renderedData[selectedIndex]; } + } + if (this.options.vue) { if (el.hasClass(ACTIVE_CLASS)) { - isMarking = false; el.removeClass(ACTIVE_CLASS); - if (field && field.length) { - this.clearField(field, isInput); - } - } else if (el.hasClass(INDETERMINATE_CLASS)) { - isMarking = true; - el.addClass(ACTIVE_CLASS); - el.removeClass(INDETERMINATE_CLASS); - if (field && field.length && value == null) { - this.clearField(field, isInput); - } - if ((!field || !field.length) && fieldName) { - this.addInput(fieldName, value, selectedObject); - } } else { - isMarking = true; - if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) { - this.dropdown.find("." + ACTIVE_CLASS).removeClass(ACTIVE_CLASS); - if (!isInput) { - this.dropdown.parent().find("input[name='" + fieldName + "']").remove(); - } - } - if (field && field.length && value == null) { - this.clearField(field, isInput); - } - // Toggle active class for the tick mark el.addClass(ACTIVE_CLASS); - if (value != null) { - if ((!field || !field.length) && fieldName) { - this.addInput(fieldName, value, selectedObject); - } else if (field && field.length) { - field.val(value).trigger('change'); - } - } } - return [selectedObject, isMarking]; - }; + return [selectedObject]; + } - GitLabDropdown.prototype.focusTextInput = function() { - if (this.options.filterable) { this.filterInput.focus(); } - }; + field = []; + value = this.options.id + ? this.options.id(selectedObject, el) + : selectedObject.id; + if (isInput) { + field = $(this.el); + } else if (value) { + field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']"); + } - GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) { - var $input; - // Create hidden input for form - $input = $('').attr('type', 'hidden').attr('name', fieldName).val(value); - if (this.options.inputId != null) { - $input.attr('id', this.options.inputId); - } - return this.dropdown.before($input); - }; - - GitLabDropdown.prototype.selectRowAtIndex = function(index) { - var $el, selector; - // If we pass an option index - if (typeof index !== "undefined") { - selector = SELECTABLE_CLASSES + ":eq(" + index + ") a"; - } else { - selector = ".dropdown-content .is-focused"; + if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) { + return; + } + + if (el.hasClass(ACTIVE_CLASS)) { + isMarking = false; + el.removeClass(ACTIVE_CLASS); + if (field && field.length) { + this.clearField(field, isInput); + } + } else if (el.hasClass(INDETERMINATE_CLASS)) { + isMarking = true; + el.addClass(ACTIVE_CLASS); + el.removeClass(INDETERMINATE_CLASS); + if (field && field.length && value == null) { + this.clearField(field, isInput); + } + if ((!field || !field.length) && fieldName) { + this.addInput(fieldName, value, selectedObject); + } + } else { + isMarking = true; + if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) { + this.dropdown.find("." + ACTIVE_CLASS).removeClass(ACTIVE_CLASS); + if (!isInput) { + this.dropdown.parent().find("input[name='" + fieldName + "']").remove(); + } } - if (this.dropdown.find(".dropdown-toggle-page").length) { - selector = ".dropdown-page-one " + selector; + if (field && field.length && value == null) { + this.clearField(field, isInput); } - // simulate a click on the first link - $el = $(selector, this.dropdown); - if ($el.length) { - var href = $el.attr('href'); - if (href && href !== '#') { - gl.utils.visitUrl(href); - } else { - $el.first().trigger('click'); + // Toggle active class for the tick mark + el.addClass(ACTIVE_CLASS); + if (value != null) { + if ((!field || !field.length) && fieldName) { + this.addInput(fieldName, value, selectedObject); + } else if (field && field.length) { + field.val(value).trigger('change'); } } - }; + } - GitLabDropdown.prototype.addArrowKeyEvent = function() { - var $input, ARROW_KEY_CODES, selector; - ARROW_KEY_CODES = [38, 40]; - $input = this.dropdown.find(".dropdown-input-field"); - selector = SELECTABLE_CLASSES; - if (this.dropdown.find(".dropdown-toggle-page").length) { - selector = ".dropdown-page-one " + selector; + return [selectedObject, isMarking]; + }; + + GitLabDropdown.prototype.focusTextInput = function() { + if (this.options.filterable) { this.filterInput.focus(); } + }; + + GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) { + var $input; + // Create hidden input for form + $input = $('').attr('type', 'hidden').attr('name', fieldName).val(value); + if (this.options.inputId != null) { + $input.attr('id', this.options.inputId); + } + return this.dropdown.before($input); + }; + + GitLabDropdown.prototype.selectRowAtIndex = function(index) { + var $el, selector; + // If we pass an option index + if (typeof index !== "undefined") { + selector = SELECTABLE_CLASSES + ":eq(" + index + ") a"; + } else { + selector = ".dropdown-content .is-focused"; + } + if (this.dropdown.find(".dropdown-toggle-page").length) { + selector = ".dropdown-page-one " + selector; + } + // simulate a click on the first link + $el = $(selector, this.dropdown); + if ($el.length) { + var href = $el.attr('href'); + if (href && href !== '#') { + gl.utils.visitUrl(href); + } else { + $el.first().trigger('click'); } - return $('body').on('keydown', (function(_this) { - return function(e) { - var $listItems, PREV_INDEX, currentKeyCode; - currentKeyCode = e.which; - if (ARROW_KEY_CODES.indexOf(currentKeyCode) !== -1) { - e.preventDefault(); - e.stopImmediatePropagation(); - PREV_INDEX = currentIndex; - $listItems = $(selector, _this.dropdown); - // if @options.filterable - // $input.blur() - if (currentKeyCode === 40) { - // Move down - if (currentIndex < ($listItems.length - 1)) { - currentIndex += 1; - } - } else if (currentKeyCode === 38) { - // Move up - if (currentIndex > 0) { - currentIndex -= 1; - } + } + }; + + GitLabDropdown.prototype.addArrowKeyEvent = function() { + var $input, ARROW_KEY_CODES, selector; + ARROW_KEY_CODES = [38, 40]; + $input = this.dropdown.find(".dropdown-input-field"); + selector = SELECTABLE_CLASSES; + if (this.dropdown.find(".dropdown-toggle-page").length) { + selector = ".dropdown-page-one " + selector; + } + return $('body').on('keydown', (function(_this) { + return function(e) { + var $listItems, PREV_INDEX, currentKeyCode; + currentKeyCode = e.which; + if (ARROW_KEY_CODES.indexOf(currentKeyCode) !== -1) { + e.preventDefault(); + e.stopImmediatePropagation(); + PREV_INDEX = currentIndex; + $listItems = $(selector, _this.dropdown); + // if @options.filterable + // $input.blur() + if (currentKeyCode === 40) { + // Move down + if (currentIndex < ($listItems.length - 1)) { + currentIndex += 1; } - if (currentIndex !== PREV_INDEX) { - _this.highlightRowAtIndex($listItems, currentIndex); + } else if (currentKeyCode === 38) { + // Move up + if (currentIndex > 0) { + currentIndex -= 1; } - return false; } - if (currentKeyCode === 13 && currentIndex !== -1) { - e.preventDefault(); - _this.selectRowAtIndex(); + if (currentIndex !== PREV_INDEX) { + _this.highlightRowAtIndex($listItems, currentIndex); } - }; - })(this)); - }; - - GitLabDropdown.prototype.removeArrayKeyEvent = function() { - return $('body').off('keydown'); - }; - - GitLabDropdown.prototype.resetRows = function resetRows() { - currentIndex = -1; - $('.is-focused', this.dropdown).removeClass('is-focused'); - }; - - GitLabDropdown.prototype.highlightRowAtIndex = function($listItems, index) { - var $dropdownContent, $listItem, dropdownContentBottom, dropdownContentHeight, dropdownContentTop, dropdownScrollTop, listItemBottom, listItemHeight, listItemTop; - // Remove the class for the previously focused row - $('.is-focused', this.dropdown).removeClass('is-focused'); - // Update the class for the row at the specific index - $listItem = $listItems.eq(index); - $listItem.find('a:first-child').addClass("is-focused"); - // Dropdown content scroll area - $dropdownContent = $listItem.closest('.dropdown-content'); - dropdownScrollTop = $dropdownContent.scrollTop(); - dropdownContentHeight = $dropdownContent.outerHeight(); - dropdownContentTop = $dropdownContent.prop('offsetTop'); - dropdownContentBottom = dropdownContentTop + dropdownContentHeight; - // Get the offset bottom of the list item - listItemHeight = $listItem.outerHeight(); - listItemTop = $listItem.prop('offsetTop'); - listItemBottom = listItemTop + listItemHeight; - if (!index) { - // Scroll the dropdown content to the top - $dropdownContent.scrollTop(0); - } else if (index === ($listItems.length - 1)) { - // Scroll the dropdown content to the bottom - $dropdownContent.scrollTop($dropdownContent.prop('scrollHeight')); - } else if (listItemBottom > (dropdownContentBottom + dropdownScrollTop)) { - // Scroll the dropdown content down - $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING); - } else if (listItemTop < (dropdownContentTop + dropdownScrollTop)) { - // Scroll the dropdown content up - return $dropdownContent.scrollTop(listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING); - } - }; + return false; + } + if (currentKeyCode === 13 && currentIndex !== -1) { + e.preventDefault(); + _this.selectRowAtIndex(); + } + }; + })(this)); + }; - GitLabDropdown.prototype.updateLabel = function(selected, el, instance) { - if (selected == null) { - selected = null; - } - if (el == null) { - el = null; - } - if (instance == null) { - instance = null; - } - return $(this.el).find(".dropdown-toggle-text").text(this.options.toggleLabel(selected, el, instance)); - }; + GitLabDropdown.prototype.removeArrayKeyEvent = function() { + return $('body').off('keydown'); + }; - GitLabDropdown.prototype.clearField = function(field, isInput) { - return isInput ? field.val('') : field.remove(); - }; + GitLabDropdown.prototype.resetRows = function resetRows() { + currentIndex = -1; + $('.is-focused', this.dropdown).removeClass('is-focused'); + }; - return GitLabDropdown; - })(); + GitLabDropdown.prototype.highlightRowAtIndex = function($listItems, index) { + var $dropdownContent, $listItem, dropdownContentBottom, dropdownContentHeight, dropdownContentTop, dropdownScrollTop, listItemBottom, listItemHeight, listItemTop; + // Remove the class for the previously focused row + $('.is-focused', this.dropdown).removeClass('is-focused'); + // Update the class for the row at the specific index + $listItem = $listItems.eq(index); + $listItem.find('a:first-child').addClass("is-focused"); + // Dropdown content scroll area + $dropdownContent = $listItem.closest('.dropdown-content'); + dropdownScrollTop = $dropdownContent.scrollTop(); + dropdownContentHeight = $dropdownContent.outerHeight(); + dropdownContentTop = $dropdownContent.prop('offsetTop'); + dropdownContentBottom = dropdownContentTop + dropdownContentHeight; + // Get the offset bottom of the list item + listItemHeight = $listItem.outerHeight(); + listItemTop = $listItem.prop('offsetTop'); + listItemBottom = listItemTop + listItemHeight; + if (!index) { + // Scroll the dropdown content to the top + $dropdownContent.scrollTop(0); + } else if (index === ($listItems.length - 1)) { + // Scroll the dropdown content to the bottom + $dropdownContent.scrollTop($dropdownContent.prop('scrollHeight')); + } else if (listItemBottom > (dropdownContentBottom + dropdownScrollTop)) { + // Scroll the dropdown content down + $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING); + } else if (listItemTop < (dropdownContentTop + dropdownScrollTop)) { + // Scroll the dropdown content up + return $dropdownContent.scrollTop(listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING); + } + }; - $.fn.glDropdown = function(opts) { - return this.each(function() { - if (!$.data(this, 'glDropdown')) { - return $.data(this, 'glDropdown', new GitLabDropdown(this, opts)); - } - }); + GitLabDropdown.prototype.updateLabel = function(selected, el, instance) { + if (selected == null) { + selected = null; + } + if (el == null) { + el = null; + } + if (instance == null) { + instance = null; + } + return $(this.el).find(".dropdown-toggle-text").text(this.options.toggleLabel(selected, el, instance)); + }; + + GitLabDropdown.prototype.clearField = function(field, isInput) { + return isInput ? field.val('') : field.remove(); }; -}).call(window); + + return GitLabDropdown; +})(); + +$.fn.glDropdown = function(opts) { + return this.each(function() { + if (!$.data(this, 'glDropdown')) { + return $.data(this, 'glDropdown', new GitLabDropdown(this, opts)); + } + }); +}; diff --git a/app/assets/javascripts/gl_field_error.js b/app/assets/javascripts/gl_field_error.js index f7cbecc0385..76de249ac3b 100644 --- a/app/assets/javascripts/gl_field_error.js +++ b/app/assets/javascripts/gl_field_error.js @@ -1,164 +1,162 @@ -/* eslint-disable no-param-reassign */ -((global) => { - /* - * This class overrides the browser's validation error bubbles, displaying custom - * error messages for invalid fields instead. To begin validating any form, add the - * class `gl-show-field-errors` to the form element, and ensure error messages are - * declared in each inputs' `title` attribute. If no title is declared for an invalid - * field the user attempts to submit, "This field is required." will be shown by default. - * - * Opt not to validate certain fields by adding the class `gl-field-error-ignore` to the input. - * - * Set a custom error anchor for error message to be injected after with the - * class `gl-field-error-anchor` - * - * Examples: - * - * Basic: - * - *
      - * - *
      - * - * Ignore specific inputs (e.g. UsernameValidator): - * - *
      - *
      - * - *
      - *
      - * - * Custom Error Anchor (allows error message to be injected after specified element): - * - *
      - *
      - * - * // Error message typically injected here - *
      - * // Error message now injected here - *
      - * - * */ - - /* - * Regex Patterns in use: - * - * Only alphanumeric: : "[a-zA-Z0-9]+" - * No special characters : "[a-zA-Z0-9-_]+", - * - * */ - - const errorMessageClass = 'gl-field-error'; - const inputErrorClass = 'gl-field-error-outline'; - const errorAnchorSelector = '.gl-field-error-anchor'; - const ignoreInputSelector = '.gl-field-error-ignore'; - - class GlFieldError { - constructor({ input, formErrors }) { - this.inputElement = $(input); - this.inputDomElement = this.inputElement.get(0); - this.form = formErrors; - this.errorMessage = this.inputElement.attr('title') || 'This field is required.'; - this.fieldErrorElement = $(`

      ${this.errorMessage}

      `); - - this.state = { - valid: false, - empty: true, - }; - - this.initFieldValidation(); - } +/** + * This class overrides the browser's validation error bubbles, displaying custom + * error messages for invalid fields instead. To begin validating any form, add the + * class `gl-show-field-errors` to the form element, and ensure error messages are + * declared in each inputs' `title` attribute. If no title is declared for an invalid + * field the user attempts to submit, "This field is required." will be shown by default. + * + * Opt not to validate certain fields by adding the class `gl-field-error-ignore` to the input. + * + * Set a custom error anchor for error message to be injected after with the + * class `gl-field-error-anchor` + * + * Examples: + * + * Basic: + * + *
      + * + *
      + * + * Ignore specific inputs (e.g. UsernameValidator): + * + *
      + *
      + * + *
      + *
      + * + * Custom Error Anchor (allows error message to be injected after specified element): + * + *
      + *
      + * + * // Error message typically injected here + *
      + * // Error message now injected here + *
      + * + */ + +/** + * Regex Patterns in use: + * + * Only alphanumeric: : "[a-zA-Z0-9]+" + * No special characters : "[a-zA-Z0-9-_]+", + * + */ + +const errorMessageClass = 'gl-field-error'; +const inputErrorClass = 'gl-field-error-outline'; +const errorAnchorSelector = '.gl-field-error-anchor'; +const ignoreInputSelector = '.gl-field-error-ignore'; + +class GlFieldError { + constructor({ input, formErrors }) { + this.inputElement = $(input); + this.inputDomElement = this.inputElement.get(0); + this.form = formErrors; + this.errorMessage = this.inputElement.attr('title') || 'This field is required.'; + this.fieldErrorElement = $(`

      ${this.errorMessage}

      `); + + this.state = { + valid: false, + empty: true, + }; + + this.initFieldValidation(); + } - initFieldValidation() { - const customErrorAnchor = this.inputElement.parents(errorAnchorSelector); - const errorAnchor = customErrorAnchor.length ? customErrorAnchor : this.inputElement; + initFieldValidation() { + const customErrorAnchor = this.inputElement.parents(errorAnchorSelector); + const errorAnchor = customErrorAnchor.length ? customErrorAnchor : this.inputElement; - // hidden when injected into DOM - errorAnchor.after(this.fieldErrorElement); - this.inputElement.off('invalid').on('invalid', this.handleInvalidSubmit.bind(this)); - this.scopedSiblings = this.safelySelectSiblings(); - } + // hidden when injected into DOM + errorAnchor.after(this.fieldErrorElement); + this.inputElement.off('invalid').on('invalid', this.handleInvalidSubmit.bind(this)); + this.scopedSiblings = this.safelySelectSiblings(); + } - safelySelectSiblings() { - // Apply `ignoreSelector` in markup to siblings whose visibility should not be toggled - const unignoredSiblings = this.inputElement.siblings(`p:not(${ignoreInputSelector})`); - const parentContainer = this.inputElement.parent('.form-group'); + safelySelectSiblings() { + // Apply `ignoreSelector` in markup to siblings whose visibility should not be toggled + const unignoredSiblings = this.inputElement.siblings(`p:not(${ignoreInputSelector})`); + const parentContainer = this.inputElement.parent('.form-group'); - // Only select siblings when they're scoped within a form-group with one input - const safelyScoped = parentContainer.length && parentContainer.find('input').length === 1; + // Only select siblings when they're scoped within a form-group with one input + const safelyScoped = parentContainer.length && parentContainer.find('input').length === 1; - return safelyScoped ? unignoredSiblings : this.fieldErrorElement; - } + return safelyScoped ? unignoredSiblings : this.fieldErrorElement; + } - renderValidity() { - this.renderClear(); + renderValidity() { + this.renderClear(); - if (this.state.valid) { - this.renderValid(); - } else if (this.state.empty) { - this.renderEmpty(); - } else if (!this.state.valid) { - this.renderInvalid(); - } + if (this.state.valid) { + this.renderValid(); + } else if (this.state.empty) { + this.renderEmpty(); + } else if (!this.state.valid) { + this.renderInvalid(); } + } - handleInvalidSubmit(event) { - event.preventDefault(); - const currentValue = this.accessCurrentValue(); - this.state.valid = false; - this.state.empty = currentValue === ''; - - this.renderValidity(); - this.form.focusOnFirstInvalid.apply(this.form); - // For UX, wait til after first invalid submission to check each keyup - this.inputElement.off('keyup.fieldValidator') - .on('keyup.fieldValidator', this.updateValidity.bind(this)); - } + handleInvalidSubmit(event) { + event.preventDefault(); + const currentValue = this.accessCurrentValue(); + this.state.valid = false; + this.state.empty = currentValue === ''; + + this.renderValidity(); + this.form.focusOnFirstInvalid.apply(this.form); + // For UX, wait til after first invalid submission to check each keyup + this.inputElement.off('keyup.fieldValidator') + .on('keyup.fieldValidator', this.updateValidity.bind(this)); + } - /* Get or set current input value */ - accessCurrentValue(newVal) { - return newVal ? this.inputElement.val(newVal) : this.inputElement.val(); - } + /* Get or set current input value */ + accessCurrentValue(newVal) { + return newVal ? this.inputElement.val(newVal) : this.inputElement.val(); + } - getInputValidity() { - return this.inputDomElement.validity.valid; - } + getInputValidity() { + return this.inputDomElement.validity.valid; + } - updateValidity() { - const inputVal = this.accessCurrentValue(); - this.state.empty = !inputVal.length; - this.state.valid = this.getInputValidity(); - this.renderValidity(); - } + updateValidity() { + const inputVal = this.accessCurrentValue(); + this.state.empty = !inputVal.length; + this.state.valid = this.getInputValidity(); + this.renderValidity(); + } - renderValid() { - return this.renderClear(); - } + renderValid() { + return this.renderClear(); + } - renderEmpty() { - return this.renderInvalid(); - } + renderEmpty() { + return this.renderInvalid(); + } - renderInvalid() { - this.inputElement.addClass(inputErrorClass); - this.scopedSiblings.hide(); - return this.fieldErrorElement.show(); - } + renderInvalid() { + this.inputElement.addClass(inputErrorClass); + this.scopedSiblings.hide(); + return this.fieldErrorElement.show(); + } - renderClear() { - const inputVal = this.accessCurrentValue(); - if (!inputVal.split(' ').length) { - const trimmedInput = inputVal.trim(); - this.accessCurrentValue(trimmedInput); - } - this.inputElement.removeClass(inputErrorClass); - this.scopedSiblings.hide(); - this.fieldErrorElement.hide(); + renderClear() { + const inputVal = this.accessCurrentValue(); + if (!inputVal.split(' ').length) { + const trimmedInput = inputVal.trim(); + this.accessCurrentValue(trimmedInput); } + this.inputElement.removeClass(inputErrorClass); + this.scopedSiblings.hide(); + this.fieldErrorElement.hide(); } +} - global.GlFieldError = GlFieldError; -})(window.gl || (window.gl = {})); +window.gl = window.gl || {}; +window.gl.GlFieldError = GlFieldError; diff --git a/app/assets/javascripts/gl_field_errors.js b/app/assets/javascripts/gl_field_errors.js index e9add115429..636258ec555 100644 --- a/app/assets/javascripts/gl_field_errors.js +++ b/app/assets/javascripts/gl_field_errors.js @@ -2,47 +2,46 @@ require('./gl_field_error'); -((global) => { - const customValidationFlag = 'gl-field-error-ignore'; - - class GlFieldErrors { - constructor(form) { - this.form = $(form); - this.state = { - inputs: [], - valid: false - }; - this.initValidators(); - } +const customValidationFlag = 'gl-field-error-ignore'; + +class GlFieldErrors { + constructor(form) { + this.form = $(form); + this.state = { + inputs: [], + valid: false + }; + this.initValidators(); + } - initValidators () { - // register selectors here as needed - const validateSelectors = [':text', ':password', '[type=email]'] - .map((selector) => `input${selector}`).join(','); + initValidators () { + // register selectors here as needed + const validateSelectors = [':text', ':password', '[type=email]'] + .map((selector) => `input${selector}`).join(','); - this.state.inputs = this.form.find(validateSelectors).toArray() - .filter((input) => !input.classList.contains(customValidationFlag)) - .map((input) => new global.GlFieldError({ input, formErrors: this })); + this.state.inputs = this.form.find(validateSelectors).toArray() + .filter((input) => !input.classList.contains(customValidationFlag)) + .map((input) => new window.gl.GlFieldError({ input, formErrors: this })); - this.form.on('submit', this.catchInvalidFormSubmit); - } + this.form.on('submit', this.catchInvalidFormSubmit); + } - /* Neccessary to prevent intercept and override invalid form submit - * because Safari & iOS quietly allow form submission when form is invalid - * and prevents disabling of invalid submit button by application.js */ + /* Neccessary to prevent intercept and override invalid form submit + * because Safari & iOS quietly allow form submission when form is invalid + * and prevents disabling of invalid submit button by application.js */ - catchInvalidFormSubmit (event) { - if (!event.currentTarget.checkValidity()) { - event.preventDefault(); - event.stopPropagation(); - } + catchInvalidFormSubmit (event) { + if (!event.currentTarget.checkValidity()) { + event.preventDefault(); + event.stopPropagation(); } + } - focusOnFirstInvalid () { - const firstInvalid = this.state.inputs.filter((input) => !input.inputDomElement.validity.valid)[0]; - firstInvalid.inputElement.focus(); - } + focusOnFirstInvalid () { + const firstInvalid = this.state.inputs.filter((input) => !input.inputDomElement.validity.valid)[0]; + firstInvalid.inputElement.focus(); } +} - global.GlFieldErrors = GlFieldErrors; -})(window.gl || (window.gl = {})); +window.gl = window.gl || {}; +window.gl.GlFieldErrors = GlFieldErrors; diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js index 0b446ff364a..e7c98e16581 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -3,90 +3,88 @@ /* global DropzoneInput */ /* global autosize */ -(() => { - const global = window.gl || (window.gl = {}); +window.gl = window.gl || {}; - function GLForm(form) { - this.form = form; - this.textarea = this.form.find('textarea.js-gfm-input'); - // Before we start, we should clean up any previous data for this form - this.destroy(); - // Setup the form - this.setupForm(); - this.form.data('gl-form', this); - } +function GLForm(form) { + this.form = form; + this.textarea = this.form.find('textarea.js-gfm-input'); + // Before we start, we should clean up any previous data for this form + this.destroy(); + // Setup the form + this.setupForm(); + this.form.data('gl-form', this); +} - GLForm.prototype.destroy = function() { - // Clean form listeners - this.clearEventListeners(); - return this.form.data('gl-form', null); - }; +GLForm.prototype.destroy = function() { + // Clean form listeners + this.clearEventListeners(); + return this.form.data('gl-form', null); +}; - GLForm.prototype.setupForm = function() { - var isNewForm; - isNewForm = this.form.is(':not(.gfm-form)'); - this.form.removeClass('js-new-note-form'); - if (isNewForm) { - this.form.find('.div-dropzone').remove(); - this.form.addClass('gfm-form'); - // remove notify commit author checkbox for non-commit notes - gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button')); - gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input')); - new DropzoneInput(this.form); - autosize(this.textarea); - // form and textarea event listeners - this.addEventListeners(); - } - gl.text.init(this.form); - // hide discard button - this.form.find('.js-note-discard').hide(); - this.form.show(); - if (this.isAutosizeable) this.setupAutosize(); - }; +GLForm.prototype.setupForm = function() { + var isNewForm; + isNewForm = this.form.is(':not(.gfm-form)'); + this.form.removeClass('js-new-note-form'); + if (isNewForm) { + this.form.find('.div-dropzone').remove(); + this.form.addClass('gfm-form'); + // remove notify commit author checkbox for non-commit notes + gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button')); + gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input')); + new DropzoneInput(this.form); + autosize(this.textarea); + // form and textarea event listeners + this.addEventListeners(); + } + gl.text.init(this.form); + // hide discard button + this.form.find('.js-note-discard').hide(); + this.form.show(); + if (this.isAutosizeable) this.setupAutosize(); +}; - GLForm.prototype.setupAutosize = function () { - this.textarea.off('autosize:resized') - .on('autosize:resized', this.setHeightData.bind(this)); +GLForm.prototype.setupAutosize = function () { + this.textarea.off('autosize:resized') + .on('autosize:resized', this.setHeightData.bind(this)); - this.textarea.off('mouseup.autosize') - .on('mouseup.autosize', this.destroyAutosize.bind(this)); + this.textarea.off('mouseup.autosize') + .on('mouseup.autosize', this.destroyAutosize.bind(this)); - setTimeout(() => { - autosize(this.textarea); - this.textarea.css('resize', 'vertical'); - }, 0); - }; + setTimeout(() => { + autosize(this.textarea); + this.textarea.css('resize', 'vertical'); + }, 0); +}; - GLForm.prototype.setHeightData = function () { - this.textarea.data('height', this.textarea.outerHeight()); - }; +GLForm.prototype.setHeightData = function () { + this.textarea.data('height', this.textarea.outerHeight()); +}; - GLForm.prototype.destroyAutosize = function () { - const outerHeight = this.textarea.outerHeight(); +GLForm.prototype.destroyAutosize = function () { + const outerHeight = this.textarea.outerHeight(); - if (this.textarea.data('height') === outerHeight) return; + if (this.textarea.data('height') === outerHeight) return; - autosize.destroy(this.textarea); + autosize.destroy(this.textarea); - this.textarea.data('height', outerHeight); - this.textarea.outerHeight(outerHeight); - this.textarea.css('max-height', window.outerHeight); - }; + this.textarea.data('height', outerHeight); + this.textarea.outerHeight(outerHeight); + this.textarea.css('max-height', window.outerHeight); +}; - GLForm.prototype.clearEventListeners = function() { - this.textarea.off('focus'); - this.textarea.off('blur'); - return gl.text.removeListeners(this.form); - }; +GLForm.prototype.clearEventListeners = function() { + this.textarea.off('focus'); + this.textarea.off('blur'); + return gl.text.removeListeners(this.form); +}; - GLForm.prototype.addEventListeners = function() { - this.textarea.on('focus', function() { - return $(this).closest('.md-area').addClass('is-focused'); - }); - return this.textarea.on('blur', function() { - return $(this).closest('.md-area').removeClass('is-focused'); - }); - }; +GLForm.prototype.addEventListeners = function() { + this.textarea.on('focus', function() { + return $(this).closest('.md-area').addClass('is-focused'); + }); + return this.textarea.on('blur', function() { + return $(this).closest('.md-area').removeClass('is-focused'); + }); +}; - global.GLForm = GLForm; -})(); +window.gl.GLForm = GLForm; diff --git a/app/assets/javascripts/group_avatar.js b/app/assets/javascripts/group_avatar.js index c5cb273c5b2..f03b47b1c1d 100644 --- a/app/assets/javascripts/group_avatar.js +++ b/app/assets/javascripts/group_avatar.js @@ -1,20 +1,19 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, one-var, one-var-declaration-per-line, no-useless-escape, max-len */ -(function() { - this.GroupAvatar = (function() { - function GroupAvatar() { - $('.js-choose-group-avatar-button').on("click", function() { - var form; - form = $(this).closest("form"); - return form.find(".js-group-avatar-input").click(); - }); - $('.js-group-avatar-input').on("change", function() { - var filename, form; - form = $(this).closest("form"); - filename = $(this).val().replace(/^.*[\\\/]/, ''); - return form.find(".js-avatar-filename").text(filename); - }); - } - return GroupAvatar; - })(); -}).call(window); +window.GroupAvatar = (function() { + function GroupAvatar() { + $('.js-choose-group-avatar-button').on("click", function() { + var form; + form = $(this).closest("form"); + return form.find(".js-group-avatar-input").click(); + }); + $('.js-group-avatar-input').on("change", function() { + var filename, form; + form = $(this).closest("form"); + filename = $(this).val().replace(/^.*[\\\/]/, ''); + return form.find(".js-avatar-filename").text(filename); + }); + } + + return GroupAvatar; +})(); diff --git a/app/assets/javascripts/group_label_subscription.js b/app/assets/javascripts/group_label_subscription.js index 15e695e81cf..7dc9ce898e8 100644 --- a/app/assets/javascripts/group_label_subscription.js +++ b/app/assets/javascripts/group_label_subscription.js @@ -1,53 +1,52 @@ /* eslint-disable func-names, object-shorthand, comma-dangle, wrap-iife, space-before-function-paren, no-param-reassign, max-len */ -(function(global) { - class GroupLabelSubscription { - constructor(container) { - const $container = $(container); - this.$dropdown = $container.find('.dropdown'); - this.$subscribeButtons = $container.find('.js-subscribe-button'); - this.$unsubscribeButtons = $container.find('.js-unsubscribe-button'); - - this.$subscribeButtons.on('click', this.subscribe.bind(this)); - this.$unsubscribeButtons.on('click', this.unsubscribe.bind(this)); - } - - unsubscribe(event) { - event.preventDefault(); - - const url = this.$unsubscribeButtons.attr('data-url'); - - $.ajax({ - type: 'POST', - url: url - }).done(() => { - this.toggleSubscriptionButtons(); - this.$unsubscribeButtons.removeAttr('data-url'); - }); - } - - subscribe(event) { - event.preventDefault(); - - const $btn = $(event.currentTarget); - const url = $btn.attr('data-url'); - - this.$unsubscribeButtons.attr('data-url', url); - - $.ajax({ - type: 'POST', - url: url - }).done(() => { - this.toggleSubscriptionButtons(); - }); - } - - toggleSubscriptionButtons() { - this.$dropdown.toggleClass('hidden'); - this.$subscribeButtons.toggleClass('hidden'); - this.$unsubscribeButtons.toggleClass('hidden'); - } +class GroupLabelSubscription { + constructor(container) { + const $container = $(container); + this.$dropdown = $container.find('.dropdown'); + this.$subscribeButtons = $container.find('.js-subscribe-button'); + this.$unsubscribeButtons = $container.find('.js-unsubscribe-button'); + + this.$subscribeButtons.on('click', this.subscribe.bind(this)); + this.$unsubscribeButtons.on('click', this.unsubscribe.bind(this)); } - global.GroupLabelSubscription = GroupLabelSubscription; -})(window.gl || (window.gl = {})); + unsubscribe(event) { + event.preventDefault(); + + const url = this.$unsubscribeButtons.attr('data-url'); + + $.ajax({ + type: 'POST', + url: url + }).done(() => { + this.toggleSubscriptionButtons(); + this.$unsubscribeButtons.removeAttr('data-url'); + }); + } + + subscribe(event) { + event.preventDefault(); + + const $btn = $(event.currentTarget); + const url = $btn.attr('data-url'); + + this.$unsubscribeButtons.attr('data-url', url); + + $.ajax({ + type: 'POST', + url: url + }).done(() => { + this.toggleSubscriptionButtons(); + }); + } + + toggleSubscriptionButtons() { + this.$dropdown.toggleClass('hidden'); + this.$subscribeButtons.toggleClass('hidden'); + this.$unsubscribeButtons.toggleClass('hidden'); + } +} + +window.gl = window.gl || {}; +window.gl.GroupLabelSubscription = GroupLabelSubscription; diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index 6b937e7fa0f..e5dfa30edab 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -1,71 +1,69 @@ /* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var, camelcase, one-var-declaration-per-line, quotes, object-shorthand, prefer-arrow-callback, comma-dangle, consistent-return, yoda, prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, max-len */ /* global Api */ -(function() { - var slice = [].slice; +var slice = [].slice; - this.GroupsSelect = (function() { - function GroupsSelect() { - $('.ajax-groups-select').each((function(_this) { - return function(i, select) { - var all_available, skip_groups; - all_available = $(select).data('all-available'); - skip_groups = $(select).data('skip-groups') || []; - return $(select).select2({ - placeholder: "Search for a group", - multiple: $(select).hasClass('multiselect'), - minimumInputLength: 0, - query: function(query) { - var options = { all_available: all_available, skip_groups: skip_groups }; - return Api.groups(query.term, options, function(groups) { - var data; - data = { - results: groups - }; - return query.callback(data); - }); - }, - initSelection: function(element, callback) { - var id; - id = $(element).val(); - if (id !== "") { - return Api.group(id, callback); - } - }, - formatResult: function() { - var args; - args = 1 <= arguments.length ? slice.call(arguments, 0) : []; - return _this.formatResult.apply(_this, args); - }, - formatSelection: function() { - var args; - args = 1 <= arguments.length ? slice.call(arguments, 0) : []; - return _this.formatSelection.apply(_this, args); - }, - dropdownCssClass: "ajax-groups-dropdown", - // we do not want to escape markup since we are displaying html in results - escapeMarkup: function(m) { - return m; +window.GroupsSelect = (function() { + function GroupsSelect() { + $('.ajax-groups-select').each((function(_this) { + return function(i, select) { + var all_available, skip_groups; + all_available = $(select).data('all-available'); + skip_groups = $(select).data('skip-groups') || []; + return $(select).select2({ + placeholder: "Search for a group", + multiple: $(select).hasClass('multiselect'), + minimumInputLength: 0, + query: function(query) { + var options = { all_available: all_available, skip_groups: skip_groups }; + return Api.groups(query.term, options, function(groups) { + var data; + data = { + results: groups + }; + return query.callback(data); + }); + }, + initSelection: function(element, callback) { + var id; + id = $(element).val(); + if (id !== "") { + return Api.group(id, callback); } - }); - }; - })(this)); - } + }, + formatResult: function() { + var args; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return _this.formatResult.apply(_this, args); + }, + formatSelection: function() { + var args; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return _this.formatSelection.apply(_this, args); + }, + dropdownCssClass: "ajax-groups-dropdown", + // we do not want to escape markup since we are displaying html in results + escapeMarkup: function(m) { + return m; + } + }); + }; + })(this)); + } - GroupsSelect.prototype.formatResult = function(group) { - var avatar; - if (group.avatar_url) { - avatar = group.avatar_url; - } else { - avatar = gon.default_avatar_url; - } - return "
      " + group.full_name + "
      " + group.full_path + "
      "; - }; + GroupsSelect.prototype.formatResult = function(group) { + var avatar; + if (group.avatar_url) { + avatar = group.avatar_url; + } else { + avatar = gon.default_avatar_url; + } + return "
      " + group.full_name + "
      " + group.full_path + "
      "; + }; - GroupsSelect.prototype.formatSelection = function(group) { - return group.full_name; - }; + GroupsSelect.prototype.formatSelection = function(group) { + return group.full_name; + }; - return GroupsSelect; - })(); -}).call(window); + return GroupsSelect; +})(); diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js index a853c3aeb1f..34f44dad7a5 100644 --- a/app/assets/javascripts/header.js +++ b/app/assets/javascripts/header.js @@ -1,8 +1,7 @@ -/* eslint-disable wrap-iife, func-names, space-before-function-paren, prefer-arrow-callback, no-var, max-len */ -(function() { - $(document).on('todo:toggle', function(e, count) { - var $todoPendingCount = $('.todos-pending-count'); - $todoPendingCount.text(gl.text.highCountTrim(count)); - $todoPendingCount.toggleClass('hidden', count === 0); - }); -})(); +/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var */ + +$(document).on('todo:toggle', function(e, count) { + var $todoPendingCount = $('.todos-pending-count'); + $todoPendingCount.text(gl.text.highCountTrim(count)); + $todoPendingCount.toggleClass('hidden', count === 0); +}); diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 604ed91627a..c078af23ef9 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -206,189 +206,187 @@ import './visibility_select'; import './wikis'; import './zen_mode'; -(function () { - document.addEventListener('beforeunload', function () { - // Unbind scroll events - $(document).off('scroll'); - // Close any open tooltips - $('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy'); - }); - - window.addEventListener('hashchange', gl.utils.handleLocationHash); - window.addEventListener('load', function onLoad() { - window.removeEventListener('load', onLoad, false); - gl.utils.handleLocationHash(); - }, false); +document.addEventListener('beforeunload', function () { + // Unbind scroll events + $(document).off('scroll'); + // Close any open tooltips + $('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy'); +}); - $(function () { - var $body = $('body'); - var $document = $(document); - var $window = $(window); - var $sidebarGutterToggle = $('.js-sidebar-toggle'); - var $flash = $('.flash-container'); - var bootstrapBreakpoint = bp.getBreakpointSize(); - var fitSidebarForSize; +window.addEventListener('hashchange', gl.utils.handleLocationHash); +window.addEventListener('load', function onLoad() { + window.removeEventListener('load', onLoad, false); + gl.utils.handleLocationHash(); +}, false); - // Set the default path for all cookies to GitLab's root directory - Cookies.defaults.path = gon.relative_url_root || '/'; +$(function () { + var $body = $('body'); + var $document = $(document); + var $window = $(window); + var $sidebarGutterToggle = $('.js-sidebar-toggle'); + var $flash = $('.flash-container'); + var bootstrapBreakpoint = bp.getBreakpointSize(); + var fitSidebarForSize; - // `hashchange` is not triggered when link target is already in window.location - $body.on('click', 'a[href^="#"]', function() { - var href = this.getAttribute('href'); - if (href.substr(1) === gl.utils.getLocationHash()) { - setTimeout(gl.utils.handleLocationHash, 1); - } - }); + // Set the default path for all cookies to GitLab's root directory + Cookies.defaults.path = gon.relative_url_root || '/'; - // prevent default action for disabled buttons - $('.btn').click(function(e) { - if ($(this).hasClass('disabled')) { - e.preventDefault(); - e.stopImmediatePropagation(); - return false; - } - }); + // `hashchange` is not triggered when link target is already in window.location + $body.on('click', 'a[href^="#"]', function() { + var href = this.getAttribute('href'); + if (href.substr(1) === gl.utils.getLocationHash()) { + setTimeout(gl.utils.handleLocationHash, 1); + } + }); - $('.js-select-on-focus').on('focusin', function () { - return $(this).select().one('mouseup', function (e) { - return e.preventDefault(); - }); - // Click a .js-select-on-focus field, select the contents - // Prevent a mouseup event from deselecting the input - }); - $('.remove-row').bind('ajax:success', function () { - $(this).tooltip('destroy') - .closest('li') - .fadeOut(); - }); - $('.js-remove-tr').bind('ajax:before', function () { - return $(this).hide(); - }); - $('.js-remove-tr').bind('ajax:success', function () { - return $(this).closest('tr').fadeOut(); - }); - $('select.select2').select2({ - width: 'resolve', - // Initialize select2 selects - dropdownAutoWidth: true - }); - $('.js-select2').bind('select2-close', function () { - return setTimeout((function () { - $('.select2-container-active').removeClass('select2-container-active'); - return $(':focus').blur(); - }), 1); - // Close select2 on escape - }); - // Initialize tooltips - $.fn.tooltip.Constructor.DEFAULTS.trigger = 'hover'; - $body.tooltip({ - selector: '.has-tooltip, [data-toggle="tooltip"]', - placement: function (tip, el) { - return $(el).data('placement') || 'bottom'; - } - }); - $('.trigger-submit').on('change', function () { - return $(this).parents('form').submit(); - // Form submitter - }); - gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), true); - // Flash - if ($flash.length > 0) { - $flash.click(function () { - return $(this).fadeOut(); - }); - $flash.show(); + // prevent default action for disabled buttons + $('.btn').click(function(e) { + if ($(this).hasClass('disabled')) { + e.preventDefault(); + e.stopImmediatePropagation(); + return false; } - // Disable form buttons while a form is submitting - $body.on('ajax:complete, ajax:beforeSend, submit', 'form', function (e) { - var buttons; - buttons = $('[type="submit"]', this); - switch (e.type) { - case 'ajax:beforeSend': - case 'submit': - return buttons.disable(); - default: - return buttons.enable(); - } - }); - $(document).ajaxError(function (e, xhrObj) { - var ref = xhrObj.status; - if (xhrObj.status === 401) { - return new Flash('You need to be logged in.', 'alert'); - } else if (ref === 404 || ref === 500) { - return new Flash('Something went wrong on our end.', 'alert'); - } - }); - $('.account-box').hover(function () { - // Show/Hide the profile menu when hovering the account box - return $(this).toggleClass('hover'); - }); - $document.on('click', '.diff-content .js-show-suppressed-diff', function () { - var $container; - $container = $(this).parent(); - $container.next('table').show(); - return $container.remove(); - // Commit show suppressed diff - }); - $('.navbar-toggle').on('click', function () { - $('.header-content .title').toggle(); - $('.header-content .header-logo').toggle(); - $('.header-content .navbar-collapse').toggle(); - return $('.navbar-toggle').toggleClass('active'); - }); - // Show/hide comments on diff - $body.on('click', '.js-toggle-diff-comments', function (e) { - var $this = $(this); - var notesHolders = $this.closest('.diff-file').find('.notes_holder'); - $this.toggleClass('active'); - if ($this.hasClass('active')) { - notesHolders.show().find('.hide, .content').show(); - } else { - notesHolders.hide().find('.content').hide(); - } - $(document).trigger('toggle.comments'); + }); + + $('.js-select-on-focus').on('focusin', function () { + return $(this).select().one('mouseup', function (e) { return e.preventDefault(); }); - $document.off('click', '.js-confirm-danger'); - $document.on('click', '.js-confirm-danger', function (e) { - var btn = $(e.target); - var form = btn.closest('form'); - var text = btn.data('confirm-danger-message'); - e.preventDefault(); - return new ConfirmDangerModal(form, text); - }); - $('input[type="search"]').each(function () { - var $this = $(this); - $this.attr('value', $this.val()); - }); - $document.off('keyup', 'input[type="search"]').on('keyup', 'input[type="search"]', function () { - var $this; - $this = $(this); - return $this.attr('value', $this.val()); - }); - $document.off('breakpoint:change').on('breakpoint:change', function (e, breakpoint) { - var $gutterIcon; - if (breakpoint === 'sm' || breakpoint === 'xs') { - $gutterIcon = $sidebarGutterToggle.find('i'); - if ($gutterIcon.hasClass('fa-angle-double-right')) { - return $sidebarGutterToggle.trigger('click'); - } - } + // Click a .js-select-on-focus field, select the contents + // Prevent a mouseup event from deselecting the input + }); + $('.remove-row').bind('ajax:success', function () { + $(this).tooltip('destroy') + .closest('li') + .fadeOut(); + }); + $('.js-remove-tr').bind('ajax:before', function () { + return $(this).hide(); + }); + $('.js-remove-tr').bind('ajax:success', function () { + return $(this).closest('tr').fadeOut(); + }); + $('select.select2').select2({ + width: 'resolve', + // Initialize select2 selects + dropdownAutoWidth: true + }); + $('.js-select2').bind('select2-close', function () { + return setTimeout((function () { + $('.select2-container-active').removeClass('select2-container-active'); + return $(':focus').blur(); + }), 1); + // Close select2 on escape + }); + // Initialize tooltips + $.fn.tooltip.Constructor.DEFAULTS.trigger = 'hover'; + $body.tooltip({ + selector: '.has-tooltip, [data-toggle="tooltip"]', + placement: function (tip, el) { + return $(el).data('placement') || 'bottom'; + } + }); + $('.trigger-submit').on('change', function () { + return $(this).parents('form').submit(); + // Form submitter + }); + gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), true); + // Flash + if ($flash.length > 0) { + $flash.click(function () { + return $(this).fadeOut(); }); - fitSidebarForSize = function () { - var oldBootstrapBreakpoint; - oldBootstrapBreakpoint = bootstrapBreakpoint; - bootstrapBreakpoint = bp.getBreakpointSize(); - if (bootstrapBreakpoint !== oldBootstrapBreakpoint) { - return $document.trigger('breakpoint:change', [bootstrapBreakpoint]); + $flash.show(); + } + // Disable form buttons while a form is submitting + $body.on('ajax:complete, ajax:beforeSend, submit', 'form', function (e) { + var buttons; + buttons = $('[type="submit"]', this); + switch (e.type) { + case 'ajax:beforeSend': + case 'submit': + return buttons.disable(); + default: + return buttons.enable(); + } + }); + $(document).ajaxError(function (e, xhrObj) { + var ref = xhrObj.status; + if (xhrObj.status === 401) { + return new Flash('You need to be logged in.', 'alert'); + } else if (ref === 404 || ref === 500) { + return new Flash('Something went wrong on our end.', 'alert'); + } + }); + $('.account-box').hover(function () { + // Show/Hide the profile menu when hovering the account box + return $(this).toggleClass('hover'); + }); + $document.on('click', '.diff-content .js-show-suppressed-diff', function () { + var $container; + $container = $(this).parent(); + $container.next('table').show(); + return $container.remove(); + // Commit show suppressed diff + }); + $('.navbar-toggle').on('click', function () { + $('.header-content .title').toggle(); + $('.header-content .header-logo').toggle(); + $('.header-content .navbar-collapse').toggle(); + return $('.navbar-toggle').toggleClass('active'); + }); + // Show/hide comments on diff + $body.on('click', '.js-toggle-diff-comments', function (e) { + var $this = $(this); + var notesHolders = $this.closest('.diff-file').find('.notes_holder'); + $this.toggleClass('active'); + if ($this.hasClass('active')) { + notesHolders.show().find('.hide, .content').show(); + } else { + notesHolders.hide().find('.content').hide(); + } + $(document).trigger('toggle.comments'); + return e.preventDefault(); + }); + $document.off('click', '.js-confirm-danger'); + $document.on('click', '.js-confirm-danger', function (e) { + var btn = $(e.target); + var form = btn.closest('form'); + var text = btn.data('confirm-danger-message'); + e.preventDefault(); + return new ConfirmDangerModal(form, text); + }); + $('input[type="search"]').each(function () { + var $this = $(this); + $this.attr('value', $this.val()); + }); + $document.off('keyup', 'input[type="search"]').on('keyup', 'input[type="search"]', function () { + var $this; + $this = $(this); + return $this.attr('value', $this.val()); + }); + $document.off('breakpoint:change').on('breakpoint:change', function (e, breakpoint) { + var $gutterIcon; + if (breakpoint === 'sm' || breakpoint === 'xs') { + $gutterIcon = $sidebarGutterToggle.find('i'); + if ($gutterIcon.hasClass('fa-angle-double-right')) { + return $sidebarGutterToggle.trigger('click'); } - }; - $window.off('resize.app').on('resize.app', function () { - return fitSidebarForSize(); - }); - gl.awardsHandler = new AwardsHandler(); - new Aside(); - - gl.utils.initTimeagoTimeout(); + } }); -}).call(window); + fitSidebarForSize = function () { + var oldBootstrapBreakpoint; + oldBootstrapBreakpoint = bootstrapBreakpoint; + bootstrapBreakpoint = bp.getBreakpointSize(); + if (bootstrapBreakpoint !== oldBootstrapBreakpoint) { + return $document.trigger('breakpoint:change', [bootstrapBreakpoint]); + } + }; + $window.off('resize.app').on('resize.app', function () { + return fitSidebarForSize(); + }); + gl.awardsHandler = new AwardsHandler(); + new Aside(); + + gl.utils.initTimeagoTimeout(); +}); -- cgit v1.2.1 From c8f986fe115d19c3f6ec014e20c7399918914d10 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 13 Mar 2017 14:08:49 -0500 Subject: fix broken variable reference --- app/assets/javascripts/abuse_reports.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/abuse_reports.js b/app/assets/javascripts/abuse_reports.js index 2934565caec..346de4ad11e 100644 --- a/app/assets/javascripts/abuse_reports.js +++ b/app/assets/javascripts/abuse_reports.js @@ -15,7 +15,7 @@ class AbuseReports { if (reportMessage.length > MAX_MESSAGE_LENGTH) { $messageCellElement.data('original-message', reportMessage); $messageCellElement.data('message-truncated', 'true'); - $messageCellElement.text(global.text.truncate(reportMessage, MAX_MESSAGE_LENGTH)); + $messageCellElement.text(window.gl.text.truncate(reportMessage, MAX_MESSAGE_LENGTH)); } } -- cgit v1.2.1 From 985af1a6707af531a242051e46a54c16dc31b9bc Mon Sep 17 00:00:00 2001 From: mhasbini Date: Mon, 13 Mar 2017 22:09:43 +0200 Subject: take nonewline context into account in diff parser --- app/views/projects/diffs/_line.html.haml | 2 +- app/views/projects/diffs/_parallel_view.html.haml | 4 +- lib/gitlab/diff/line.rb | 6 +-- lib/gitlab/diff/parser.rb | 6 ++- spec/lib/gitlab/diff/parser_spec.rb | 48 +++++++++++++++++++++++ 5 files changed, 59 insertions(+), 7 deletions(-) diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index 62135d3ae32..c09c7b87e24 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -9,7 +9,7 @@ - case type - when 'match' = diff_match_line line.old_pos, line.new_pos, text: line.text - - when 'nonewline' + - when 'old-nonewline', 'new-nonewline' %td.old_line.diff-line-num %td.new_line.diff-line-num %td.line_content.match= line.text diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index e7758c8bdfa..b7346f27ddb 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -12,7 +12,7 @@ - case left.type - when 'match' = diff_match_line left.old_pos, nil, text: left.text, view: :parallel - - when 'nonewline' + - when 'old-nonewline', 'new-nonewline' %td.old_line.diff-line-num %td.line_content.match= left.text - else @@ -31,7 +31,7 @@ - case right.type - when 'match' = diff_match_line nil, right.new_pos, text: left.text, view: :parallel - - when 'nonewline' + - when 'old-nonewline', 'new-nonewline' %td.new_line.diff-line-num %td.line_content.match= right.text - else diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb index 80a146b4a5a..114656958e3 100644 --- a/lib/gitlab/diff/line.rb +++ b/lib/gitlab/diff/line.rb @@ -38,11 +38,11 @@ module Gitlab end def added? - type == 'new' + type == 'new' || type == 'new-nonewline' end def removed? - type == 'old' + type == 'old' || type == 'old-nonewline' end def rich_text @@ -52,7 +52,7 @@ module Gitlab end def meta? - type == 'match' || type == 'nonewline' + type == 'match' end def as_json(opts = nil) diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb index 8f844224a7a..742f989c50b 100644 --- a/lib/gitlab/diff/parser.rb +++ b/lib/gitlab/diff/parser.rb @@ -11,6 +11,7 @@ module Gitlab line_old = 1 line_new = 1 type = nil + context = nil # By returning an Enumerator we make it possible to search for a single line (with #find) # without having to instantiate all the others that come after it. @@ -31,7 +32,8 @@ module Gitlab line_obj_index += 1 next elsif line[0] == '\\' - type = 'nonewline' + type = "#{context}-nonewline" + yielder << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new) line_obj_index += 1 else @@ -43,8 +45,10 @@ module Gitlab case line[0] when "+" line_new += 1 + context = :new when "-" line_old += 1 + context = :old when "\\" # rubocop:disable Lint/EmptyWhen # No increment else diff --git a/spec/lib/gitlab/diff/parser_spec.rb b/spec/lib/gitlab/diff/parser_spec.rb index b983d73f8be..e76128ecd87 100644 --- a/spec/lib/gitlab/diff/parser_spec.rb +++ b/spec/lib/gitlab/diff/parser_spec.rb @@ -91,6 +91,54 @@ eos end end + describe '\ No newline at end of file' do + it "parses nonewline in one file correctly" do + first_nonewline_diff = <<~END + --- a/test + +++ b/test + @@ -1,2 +1,2 @@ + +ipsum + lorem + -ipsum + \\ No newline at end of file + END + lines = parser.parse(first_nonewline_diff.lines).to_a + + expect(lines[0].type).to eq('new') + expect(lines[0].text).to eq('+ipsum') + expect(lines[2].type).to eq('old') + expect(lines[3].type).to eq('old-nonewline') + expect(lines[1].old_pos).to eq(1) + expect(lines[1].new_pos).to eq(2) + end + + it "parses nonewline in two files correctly" do + both_nonewline_diff = <<~END + --- a/test + +++ b/test + @@ -1,2 +1,2 @@ + -lorem + -ipsum + \\ No newline at end of file + +ipsum + +lorem + \\ No newline at end of file + END + lines = parser.parse(both_nonewline_diff.lines).to_a + + expect(lines[0].type).to eq('old') + expect(lines[1].type).to eq('old') + expect(lines[2].type).to eq('old-nonewline') + expect(lines[5].type).to eq('new-nonewline') + expect(lines[3].text).to eq('+ipsum') + expect(lines[3].old_pos).to eq(3) + expect(lines[3].new_pos).to eq(1) + expect(lines[4].text).to eq('+lorem') + expect(lines[4].old_pos).to eq(3) + expect(lines[4].new_pos).to eq(2) + end + end + context 'when lines is empty' do it { expect(parser.parse([])).to eq([]) } it { expect(parser.parse(nil)).to eq([]) } -- cgit v1.2.1 From 37ce638ccd476144fe9235a4d091be4135a3e00a Mon Sep 17 00:00:00 2001 From: blackst0ne Date: Tue, 14 Mar 2017 07:24:30 +1100 Subject: Use dig --- app/views/projects/new.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 560e3439fc2..d129da943f8 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -1,6 +1,6 @@ - page_title 'New Project' - header_title "Projects", dashboard_projects_path -- visibility_level = params.try(:[], :project).try(:[], :visibility_level) || default_project_visibility +- visibility_level = params.dig(:project, :visibility_level) || default_project_visibility .project-edit-container .project-edit-errors -- cgit v1.2.1 From 11dfad3e3a3209532184d828c1f70e6b774ade71 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 20 Jan 2017 15:13:27 -0800 Subject: Add performance/scalability concerns to CONTRIBUTING.md [ci skip] --- CONTRIBUTING.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1fd29fef4f0..9a6e3feec4c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -399,6 +399,12 @@ There are a few rules to get your merge request accepted: 1. Contains functionality we think other users will benefit from too 1. Doesn't add configuration options or settings options since they complicate making and testing future changes +1. Changes do not adversely degrade performance. + - Avoid repeated polling of endpoints that require a significant amount of overhead + - Check for N+1 queries via the SQL log or [`QueryRecorder`](https://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html) + - Avoid repeated access of filesystem +1. If you need polling to support real-time features, consider using this [described long + polling approach](https://gitlab.com/gitlab-org/gitlab-ce/issues/26926). 1. Changes after submitting the merge request should be in separate commits (no squashing). If necessary, you will be asked to squash when the review is over, before merging. @@ -434,6 +440,7 @@ the feature you contribute through all of these steps. 1. Description explaining the relevancy (see following item) 1. Working and clean code that is commented where needed 1. Unit and integration tests that pass on the CI server +1. Performance/scalability implications have been considered, addressed, and tested 1. [Documented][doc-styleguide] in the /doc directory 1. Changelog entry added 1. Reviewed and any concerns are addressed -- cgit v1.2.1 From 29e0cb4b91b3800ef4974c54b8473e6e4fb28e16 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 13 Mar 2017 21:48:32 +0000 Subject: Organize our polyfills and standardize on core-js --- app/assets/javascripts/behaviors/quick_submit.js | 2 +- app/assets/javascripts/behaviors/requires_input.js | 2 +- app/assets/javascripts/commons/bootstrap.js | 8 ++++- app/assets/javascripts/commons/index.js | 1 + app/assets/javascripts/commons/polyfills.js | 10 ++++++ .../javascripts/commons/polyfills/custom_event.js | 9 +++++ .../javascripts/commons/polyfills/element.js | 20 +++++++++++ app/assets/javascripts/droplab/droplab_ajax.js | 3 ++ .../javascripts/droplab/droplab_ajax_filter.js | 3 ++ app/assets/javascripts/extensions/array.js | 28 ++++----------- app/assets/javascripts/extensions/custom_event.js | 12 ------- app/assets/javascripts/extensions/element.js | 20 ----------- app/assets/javascripts/extensions/jquery.js | 16 --------- app/assets/javascripts/extensions/object.js | 26 -------------- app/assets/javascripts/extensions/string.js | 2 -- .../filtered_search_dropdown_manager.js | 4 +++ .../filtered_search/filtered_search_manager.js | 5 +-- app/assets/javascripts/main.js | 8 ----- .../javascripts/monitoring/prometheus_graph.js | 6 ++-- changelogs/unreleased/use-corejs-polyfills.yml | 4 +++ package.json | 4 +-- spec/javascripts/awards_handler_spec.js | 3 -- .../blob/create_branch_dropdown_spec.js | 2 -- .../blob/target_branch_dropdown_spec.js | 2 -- spec/javascripts/boards/board_new_issue_spec.js | 1 - spec/javascripts/boards/boards_store_spec.js | 1 - spec/javascripts/bootstrap_jquery_spec.js | 42 ++++++++++++++++++++++ spec/javascripts/extensions/array_spec.js | 23 ------------ spec/javascripts/extensions/element_spec.js | 38 -------------------- spec/javascripts/extensions/jquery_spec.js | 42 ---------------------- spec/javascripts/extensions/object_spec.js | 25 ------------- .../filtered_search_manager_spec.js | 5 ++- spec/javascripts/gl_emoji_spec.js | 3 -- .../monitoring/prometheus_graph_spec.js | 3 -- spec/javascripts/polyfills/element_spec.js | 36 +++++++++++++++++++ spec/javascripts/right_sidebar_spec.js | 4 +-- spec/javascripts/shortcuts_issuable_spec.js | 8 ++--- yarn.lock | 12 ++----- 38 files changed, 165 insertions(+), 278 deletions(-) create mode 100644 app/assets/javascripts/commons/polyfills.js create mode 100644 app/assets/javascripts/commons/polyfills/custom_event.js create mode 100644 app/assets/javascripts/commons/polyfills/element.js delete mode 100644 app/assets/javascripts/extensions/custom_event.js delete mode 100644 app/assets/javascripts/extensions/element.js delete mode 100644 app/assets/javascripts/extensions/jquery.js delete mode 100644 app/assets/javascripts/extensions/object.js delete mode 100644 app/assets/javascripts/extensions/string.js create mode 100644 changelogs/unreleased/use-corejs-polyfills.yml create mode 100644 spec/javascripts/bootstrap_jquery_spec.js delete mode 100644 spec/javascripts/extensions/element_spec.js delete mode 100644 spec/javascripts/extensions/jquery_spec.js delete mode 100644 spec/javascripts/extensions/object_spec.js create mode 100644 spec/javascripts/polyfills/element_spec.js diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js index a7e68ae5cb9..626f3503c91 100644 --- a/app/assets/javascripts/behaviors/quick_submit.js +++ b/app/assets/javascripts/behaviors/quick_submit.js @@ -6,7 +6,7 @@ // "Meta+Enter" (Mac) or "Ctrl+Enter" (Linux/Windows) key combination, the form // is submitted. // -require('../extensions/jquery'); +import '../commons/bootstrap'; // // ### Example Markup diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js index 6b21695d082..eb7143f5b1a 100644 --- a/app/assets/javascripts/behaviors/requires_input.js +++ b/app/assets/javascripts/behaviors/requires_input.js @@ -4,7 +4,7 @@ // When called on a form with input fields with the `required` attribute, the // form's submit button will be disabled until all required fields have values. // -require('../extensions/jquery'); +import '../commons/bootstrap'; // // ### Example Markup diff --git a/app/assets/javascripts/commons/bootstrap.js b/app/assets/javascripts/commons/bootstrap.js index db0cbfd87c3..36bfe457be9 100644 --- a/app/assets/javascripts/commons/bootstrap.js +++ b/app/assets/javascripts/commons/bootstrap.js @@ -1,4 +1,4 @@ -import 'jquery'; +import $ from 'jquery'; // bootstrap jQuery plugins import 'bootstrap-sass/assets/javascripts/bootstrap/affix'; @@ -8,3 +8,9 @@ import 'bootstrap-sass/assets/javascripts/bootstrap/modal'; import 'bootstrap-sass/assets/javascripts/bootstrap/tab'; import 'bootstrap-sass/assets/javascripts/bootstrap/transition'; import 'bootstrap-sass/assets/javascripts/bootstrap/tooltip'; + +// custom jQuery functions +$.fn.extend({ + disable() { return $(this).attr('disabled', 'disabled').addClass('disabled'); }, + enable() { return $(this).removeAttr('disabled').removeClass('disabled'); }, +}); diff --git a/app/assets/javascripts/commons/index.js b/app/assets/javascripts/commons/index.js index 72ede1d621a..7063f59d446 100644 --- a/app/assets/javascripts/commons/index.js +++ b/app/assets/javascripts/commons/index.js @@ -1,2 +1,3 @@ +import './polyfills'; import './jquery'; import './bootstrap'; diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js new file mode 100644 index 00000000000..fbd0db64ca7 --- /dev/null +++ b/app/assets/javascripts/commons/polyfills.js @@ -0,0 +1,10 @@ +// ECMAScript polyfills +import 'core-js/fn/array/find'; +import 'core-js/fn/object/assign'; +import 'core-js/fn/promise'; +import 'core-js/fn/string/code-point-at'; +import 'core-js/fn/string/from-code-point'; + +// Browser polyfills +import './polyfills/custom_event'; +import './polyfills/element'; diff --git a/app/assets/javascripts/commons/polyfills/custom_event.js b/app/assets/javascripts/commons/polyfills/custom_event.js new file mode 100644 index 00000000000..aea61b82d03 --- /dev/null +++ b/app/assets/javascripts/commons/polyfills/custom_event.js @@ -0,0 +1,9 @@ +if (typeof window.CustomEvent !== 'function') { + window.CustomEvent = function CustomEvent(event, params) { + const evt = document.createEvent('CustomEvent'); + const evtParams = params || { bubbles: false, cancelable: false, detail: undefined }; + evt.initCustomEvent(event, evtParams.bubbles, evtParams.cancelable, evtParams.detail); + return evt; + }; + window.CustomEvent.prototype = Event; +} diff --git a/app/assets/javascripts/commons/polyfills/element.js b/app/assets/javascripts/commons/polyfills/element.js new file mode 100644 index 00000000000..9a1f73bf2ac --- /dev/null +++ b/app/assets/javascripts/commons/polyfills/element.js @@ -0,0 +1,20 @@ +Element.prototype.closest = Element.prototype.closest || + function closest(selector, selectedElement = this) { + if (!selectedElement) return null; + return selectedElement.matches(selector) ? + selectedElement : + Element.prototype.closest(selector, selectedElement.parentElement); + }; + +Element.prototype.matches = Element.prototype.matches || + Element.prototype.matchesSelector || + Element.prototype.mozMatchesSelector || + Element.prototype.msMatchesSelector || + Element.prototype.oMatchesSelector || + Element.prototype.webkitMatchesSelector || + function matches(selector) { + const elms = (this.document || this.ownerDocument).querySelectorAll(selector); + let i = elms.length - 1; + while (i >= 0 && elms.item(i) !== this) { i -= 1; } + return i > -1; + }; diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js index f61be741b4a..020f8b4ac65 100644 --- a/app/assets/javascripts/droplab/droplab_ajax.js +++ b/app/assets/javascripts/droplab/droplab_ajax.js @@ -74,6 +74,9 @@ require('../window')(function(w){ this._loadUrlData(config.endpoint) .then(function(d) { self._loadData(d, config, self); + }, function(xhrError) { + // TODO: properly handle errors due to XHR cancellation + return; }).catch(function(e) { throw new droplabAjaxException(e.message || e); }); diff --git a/app/assets/javascripts/droplab/droplab_ajax_filter.js b/app/assets/javascripts/droplab/droplab_ajax_filter.js index b63d73066cb..05eba7aef56 100644 --- a/app/assets/javascripts/droplab/droplab_ajax_filter.js +++ b/app/assets/javascripts/droplab/droplab_ajax_filter.js @@ -82,6 +82,9 @@ require('../window')(function(w){ this._loadUrlData(url) .then(function(data) { self._loadData(data, config, self); + }, function(xhrError) { + // TODO: properly handle errors due to XHR cancellation + return; }); } }, diff --git a/app/assets/javascripts/extensions/array.js b/app/assets/javascripts/extensions/array.js index f8256a8d26d..027222f804d 100644 --- a/app/assets/javascripts/extensions/array.js +++ b/app/assets/javascripts/extensions/array.js @@ -1,27 +1,11 @@ -/* eslint-disable no-extend-native, func-names, space-before-function-paren, space-infix-ops, strict, max-len */ +// TODO: remove this -'use strict'; - -Array.prototype.first = function() { +// eslint-disable-next-line no-extend-native +Array.prototype.first = function first() { return this[0]; }; -Array.prototype.last = function() { - return this[this.length-1]; -}; - -Array.prototype.find = Array.prototype.find || function(predicate, ...args) { - if (!this) throw new TypeError('Array.prototype.find called on null or undefined'); - if (typeof predicate !== 'function') throw new TypeError('predicate must be a function'); - - const list = Object(this); - const thisArg = args[1]; - let value = {}; - - for (let i = 0; i < list.length; i += 1) { - value = list[i]; - if (predicate.call(thisArg, value, i, list)) return value; - } - - return undefined; +// eslint-disable-next-line no-extend-native +Array.prototype.last = function last() { + return this[this.length - 1]; }; diff --git a/app/assets/javascripts/extensions/custom_event.js b/app/assets/javascripts/extensions/custom_event.js deleted file mode 100644 index abedae4c1c7..00000000000 --- a/app/assets/javascripts/extensions/custom_event.js +++ /dev/null @@ -1,12 +0,0 @@ -/* global CustomEvent */ -/* eslint-disable no-global-assign */ - -// Custom event support for IE -CustomEvent = function CustomEvent(event, parameters) { - const params = parameters || { bubbles: false, cancelable: false, detail: undefined }; - const evt = document.createEvent('CustomEvent'); - evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); - return evt; -}; - -CustomEvent.prototype = window.Event.prototype; diff --git a/app/assets/javascripts/extensions/element.js b/app/assets/javascripts/extensions/element.js deleted file mode 100644 index 90ab79305a7..00000000000 --- a/app/assets/javascripts/extensions/element.js +++ /dev/null @@ -1,20 +0,0 @@ -/* global Element */ -/* eslint-disable consistent-return, max-len, no-empty, func-names */ - -Element.prototype.closest = Element.prototype.closest || function closest(selector, selectedElement = this) { - if (!selectedElement) return; - return selectedElement.matches(selector) ? selectedElement : Element.prototype.closest(selector, selectedElement.parentElement); -}; - -Element.prototype.matches = Element.prototype.matches || - Element.prototype.matchesSelector || - Element.prototype.mozMatchesSelector || - Element.prototype.msMatchesSelector || - Element.prototype.oMatchesSelector || - Element.prototype.webkitMatchesSelector || - function (s) { - const matches = (this.document || this.ownerDocument).querySelectorAll(s); - let i = matches.length - 1; - while (i >= 0 && matches.item(i) !== this) { i -= 1; } - return i > -1; - }; diff --git a/app/assets/javascripts/extensions/jquery.js b/app/assets/javascripts/extensions/jquery.js deleted file mode 100644 index 1a489b859e8..00000000000 --- a/app/assets/javascripts/extensions/jquery.js +++ /dev/null @@ -1,16 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, object-shorthand, comma-dangle, max-len */ -// Disable an element and add the 'disabled' Bootstrap class -(function() { - $.fn.extend({ - disable: function() { - return $(this).attr('disabled', 'disabled').addClass('disabled'); - } - }); - - // Enable an element and remove the 'disabled' Bootstrap class - $.fn.extend({ - enable: function() { - return $(this).removeAttr('disabled').removeClass('disabled'); - } - }); -}).call(window); diff --git a/app/assets/javascripts/extensions/object.js b/app/assets/javascripts/extensions/object.js deleted file mode 100644 index 70a2d765abd..00000000000 --- a/app/assets/javascripts/extensions/object.js +++ /dev/null @@ -1,26 +0,0 @@ -/* eslint-disable no-restricted-syntax */ - -// Adapted from https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill -if (typeof Object.assign !== 'function') { - Object.assign = function assign(target, ...args) { - if (target == null) { // TypeError if undefined or null - throw new TypeError('Cannot convert undefined or null to object'); - } - - const to = Object(target); - - for (let index = 0; index < args.length; index += 1) { - const nextSource = args[index]; - - if (nextSource != null) { // Skip over if undefined or null - for (const nextKey in nextSource) { - // Avoid bugs when hasOwnProperty is shadowed - if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { - to[nextKey] = nextSource[nextKey]; - } - } - } - } - return to; - }; -} diff --git a/app/assets/javascripts/extensions/string.js b/app/assets/javascripts/extensions/string.js deleted file mode 100644 index ae9662444b0..00000000000 --- a/app/assets/javascripts/extensions/string.js +++ /dev/null @@ -1,2 +0,0 @@ -import 'string.prototype.codepointat'; -import 'string.fromcodepoint'; diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js index e1a97070439..d37c812c1f7 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -162,6 +162,10 @@ } resetDropdowns() { + if (!this.currentDropdown) { + return; + } + // Force current dropdown to hide this.mapping[this.currentDropdown].reference.hideDropdown(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 638fe744668..835e87a28d7 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -38,7 +38,8 @@ this.editTokenWrapper = this.editToken.bind(this); this.tokenChange = this.tokenChange.bind(this); - this.filteredSearchInput.form.addEventListener('submit', this.handleFormSubmit); + this.filteredSearchInputForm = this.filteredSearchInput.form; + this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit); this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper); this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper); this.filteredSearchInput.addEventListener('input', this.handleInputPlaceholderWrapper); @@ -56,7 +57,7 @@ } unbindEvents() { - this.filteredSearchInput.form.removeEventListener('submit', this.handleFormSubmit); + this.filteredSearchInputForm.removeEventListener('submit', this.handleFormSubmit); this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper); this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper); this.filteredSearchInput.removeEventListener('input', this.handleInputPlaceholderWrapper); diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 604ed91627a..cf3e4ee77b6 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -16,17 +16,9 @@ import Sortable from 'vendor/Sortable'; import 'mousetrap'; import 'mousetrap/plugins/pause/mousetrap-pause'; import 'vendor/fuzzaldrin-plus'; -import promisePolyfill from 'es6-promise'; // extensions -import './extensions/string'; import './extensions/array'; -import './extensions/custom_event'; -import './extensions/element'; -import './extensions/jquery'; -import './extensions/object'; - -promisePolyfill.polyfill(); // expose common libraries as globals (TODO: remove these) window.jQuery = jQuery; diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js index 9384fe3f276..71eb746edac 100644 --- a/app/assets/javascripts/monitoring/prometheus_graph.js +++ b/app/assets/javascripts/monitoring/prometheus_graph.js @@ -1,9 +1,11 @@ -/* eslint-disable no-new*/ +/* eslint-disable no-new */ +/* global Flash */ + import d3 from 'd3'; import _ from 'underscore'; import statusCodes from '~/lib/utils/http_status'; import '~/lib/utils/common_utils'; -import Flash from '~/flash'; +import '~/flash'; const prometheusGraphsContainer = '.prometheus-graph'; const metricsEndpoint = 'metrics.json'; diff --git a/changelogs/unreleased/use-corejs-polyfills.yml b/changelogs/unreleased/use-corejs-polyfills.yml new file mode 100644 index 00000000000..381f80c5c0d --- /dev/null +++ b/changelogs/unreleased/use-corejs-polyfills.yml @@ -0,0 +1,4 @@ +--- +title: Standardize on core-js for es2015 polyfills +merge_request: 9749 +author: diff --git a/package.json b/package.json index efa3a63e693..9652dd8f972 100644 --- a/package.json +++ b/package.json @@ -17,11 +17,11 @@ "babel-preset-stage-2": "^6.22.0", "bootstrap-sass": "^3.3.6", "compression-webpack-plugin": "^0.3.2", + "core-js": "^2.4.1", "d3": "^3.5.11", "document-register-element": "^1.3.0", "dropzone": "^4.2.0", "emoji-unicode-version": "^0.2.1", - "es6-promise": "^4.0.5", "jquery": "^2.2.1", "jquery-ujs": "^1.2.1", "js-cookie": "^2.1.3", @@ -31,8 +31,6 @@ "raw-loader": "^0.5.1", "select2": "3.5.2-browserify", "stats-webpack-plugin": "^0.4.3", - "string.fromcodepoint": "^0.2.1", - "string.prototype.codepointat": "^0.2.0", "timeago.js": "^2.0.5", "underscore": "^1.8.3", "vue": "^2.1.10", diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index 9a2978006aa..0a6e042b700 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -1,11 +1,8 @@ /* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, max-len */ -import promisePolyfill from 'es6-promise'; import Cookies from 'js-cookie'; import AwardsHandler from '~/awards_handler'; -promisePolyfill.polyfill(); - (function() { var awardsHandler, lazyAssert, urlRoot, openAndWaitForEmojiMenu; diff --git a/spec/javascripts/blob/create_branch_dropdown_spec.js b/spec/javascripts/blob/create_branch_dropdown_spec.js index dafb43761e0..c1179e572ae 100644 --- a/spec/javascripts/blob/create_branch_dropdown_spec.js +++ b/spec/javascripts/blob/create_branch_dropdown_spec.js @@ -1,5 +1,3 @@ -require('jquery'); -require('~/extensions/jquery.js'); require('~/gl_dropdown'); require('~/lib/utils/type_utility'); require('~/blob/create_branch_dropdown'); diff --git a/spec/javascripts/blob/target_branch_dropdown_spec.js b/spec/javascripts/blob/target_branch_dropdown_spec.js index 6f3eb4cc7eb..4fb79663c51 100644 --- a/spec/javascripts/blob/target_branch_dropdown_spec.js +++ b/spec/javascripts/blob/target_branch_dropdown_spec.js @@ -1,5 +1,3 @@ -require('jquery'); -require('~/extensions/jquery.js'); require('~/gl_dropdown'); require('~/lib/utils/type_utility'); require('~/blob/create_branch_dropdown'); diff --git a/spec/javascripts/boards/board_new_issue_spec.js b/spec/javascripts/boards/board_new_issue_spec.js index 22c9f12951b..4999933c0c1 100644 --- a/spec/javascripts/boards/board_new_issue_spec.js +++ b/spec/javascripts/boards/board_new_issue_spec.js @@ -8,7 +8,6 @@ import boardNewIssue from '~/boards/components/board_new_issue'; require('~/boards/models/list'); require('./mock_data'); -require('es6-promise').polyfill(); describe('Issue boards new issue form', () => { let vm; diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js index 49a2ca4a78f..1d1069600fc 100644 --- a/spec/javascripts/boards/boards_store_spec.js +++ b/spec/javascripts/boards/boards_store_spec.js @@ -15,7 +15,6 @@ require('~/boards/models/user'); require('~/boards/services/board_service'); require('~/boards/stores/boards_store'); require('./mock_data'); -require('es6-promise').polyfill(); describe('Store', () => { beforeEach(() => { diff --git a/spec/javascripts/bootstrap_jquery_spec.js b/spec/javascripts/bootstrap_jquery_spec.js new file mode 100644 index 00000000000..48994b7c523 --- /dev/null +++ b/spec/javascripts/bootstrap_jquery_spec.js @@ -0,0 +1,42 @@ +/* eslint-disable space-before-function-paren, no-var */ + +import '~/commons/bootstrap'; + +(function() { + describe('Bootstrap jQuery extensions', function() { + describe('disable', function() { + beforeEach(function() { + return setFixtures(''); + }); + it('adds the disabled attribute', function() { + var $input; + $input = $('input').first(); + $input.disable(); + return expect($input).toHaveAttr('disabled', 'disabled'); + }); + return it('adds the disabled class', function() { + var $input; + $input = $('input').first(); + $input.disable(); + return expect($input).toHaveClass('disabled'); + }); + }); + return describe('enable', function() { + beforeEach(function() { + return setFixtures(''); + }); + it('removes the disabled attribute', function() { + var $input; + $input = $('input').first(); + $input.enable(); + return expect($input).not.toHaveAttr('disabled'); + }); + return it('removes the disabled class', function() { + var $input; + $input = $('input').first(); + $input.enable(); + return expect($input).not.toHaveClass('disabled'); + }); + }); + }); +}).call(window); diff --git a/spec/javascripts/extensions/array_spec.js b/spec/javascripts/extensions/array_spec.js index 60f6b9b78e3..4b871fe967d 100644 --- a/spec/javascripts/extensions/array_spec.js +++ b/spec/javascripts/extensions/array_spec.js @@ -18,28 +18,5 @@ require('~/extensions/array'); return expect(arr.last()).toBe(5); }); }); - - describe('find', function () { - beforeEach(() => { - this.arr = [0, 1, 2, 3, 4, 5]; - }); - - it('returns the item that first passes the predicate function', () => { - expect(this.arr.find(item => item === 2)).toBe(2); - }); - - it('returns undefined if no items pass the predicate function', () => { - expect(this.arr.find(item => item === 6)).not.toBeDefined(); - }); - - it('error when called on undefined or null', () => { - expect(Array.prototype.find.bind(undefined, item => item === 1)).toThrow(); - expect(Array.prototype.find.bind(null, item => item === 1)).toThrow(); - }); - - it('error when predicate is not a function', () => { - expect(Array.prototype.find.bind(this.arr, 1)).toThrow(); - }); - }); }); }).call(window); diff --git a/spec/javascripts/extensions/element_spec.js b/spec/javascripts/extensions/element_spec.js deleted file mode 100644 index 2d8a128ed33..00000000000 --- a/spec/javascripts/extensions/element_spec.js +++ /dev/null @@ -1,38 +0,0 @@ -require('~/extensions/element'); - -(() => { - describe('Element extensions', function () { - beforeEach(() => { - this.element = document.createElement('ul'); - }); - - describe('matches', () => { - it('returns true if element matches the selector', () => { - expect(this.element.matches('ul')).toBeTruthy(); - }); - - it("returns false if element doesn't match the selector", () => { - expect(this.element.matches('.not-an-element')).toBeFalsy(); - }); - }); - - describe('closest', () => { - beforeEach(() => { - this.childElement = document.createElement('li'); - this.element.appendChild(this.childElement); - }); - - it('returns the closest parent that matches the selector', () => { - expect(this.childElement.closest('ul').toString()).toBe(this.element.toString()); - }); - - it('returns itself if it matches the selector', () => { - expect(this.childElement.closest('li').toString()).toBe(this.childElement.toString()); - }); - - it('returns undefined if nothing matches the selector', () => { - expect(this.childElement.closest('.no-an-element')).toBeFalsy(); - }); - }); - }); -})(); diff --git a/spec/javascripts/extensions/jquery_spec.js b/spec/javascripts/extensions/jquery_spec.js deleted file mode 100644 index 096d3272eac..00000000000 --- a/spec/javascripts/extensions/jquery_spec.js +++ /dev/null @@ -1,42 +0,0 @@ -/* eslint-disable space-before-function-paren, no-var */ - -require('~/extensions/jquery'); - -(function() { - describe('jQuery extensions', function() { - describe('disable', function() { - beforeEach(function() { - return setFixtures(''); - }); - it('adds the disabled attribute', function() { - var $input; - $input = $('input').first(); - $input.disable(); - return expect($input).toHaveAttr('disabled', 'disabled'); - }); - return it('adds the disabled class', function() { - var $input; - $input = $('input').first(); - $input.disable(); - return expect($input).toHaveClass('disabled'); - }); - }); - return describe('enable', function() { - beforeEach(function() { - return setFixtures(''); - }); - it('removes the disabled attribute', function() { - var $input; - $input = $('input').first(); - $input.enable(); - return expect($input).not.toHaveAttr('disabled'); - }); - return it('removes the disabled class', function() { - var $input; - $input = $('input').first(); - $input.enable(); - return expect($input).not.toHaveClass('disabled'); - }); - }); - }); -}).call(window); diff --git a/spec/javascripts/extensions/object_spec.js b/spec/javascripts/extensions/object_spec.js deleted file mode 100644 index 2467ed78459..00000000000 --- a/spec/javascripts/extensions/object_spec.js +++ /dev/null @@ -1,25 +0,0 @@ -require('~/extensions/object'); - -describe('Object extensions', () => { - describe('assign', () => { - it('merges source object into target object', () => { - const targetObj = {}; - const sourceObj = { - foo: 'bar', - }; - Object.assign(targetObj, sourceObj); - expect(targetObj.foo).toBe('bar'); - }); - - it('merges object with the same properties', () => { - const targetObj = { - foo: 'bar', - }; - const sourceObj = { - foo: 'baz', - }; - Object.assign(targetObj, sourceObj); - expect(targetObj.foo).toBe('baz'); - }); - }); -}); diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js index 81c1d81d181..ae9c263d1d7 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js @@ -41,7 +41,6 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper `); - spyOn(gl.FilteredSearchManager.prototype, 'cleanup').and.callFake(() => {}); spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {}); spyOn(gl.FilteredSearchManager.prototype, 'tokenChange').and.callFake(() => {}); spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {}); @@ -54,6 +53,10 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper manager = new gl.FilteredSearchManager(); }); + afterEach(() => { + manager.cleanup(); + }); + describe('search', () => { const defaultParams = '?scope=all&utf8=✓&state=opened'; diff --git a/spec/javascripts/gl_emoji_spec.js b/spec/javascripts/gl_emoji_spec.js index 7ab0b37f2ec..9b44b25980c 100644 --- a/spec/javascripts/gl_emoji_spec.js +++ b/spec/javascripts/gl_emoji_spec.js @@ -1,6 +1,3 @@ -import '~/extensions/string'; -import '~/extensions/array'; - import { glEmojiTag } from '~/behaviors/gl_emoji'; import { isEmojiUnicodeSupported, diff --git a/spec/javascripts/monitoring/prometheus_graph_spec.js b/spec/javascripts/monitoring/prometheus_graph_spec.js index 823b4bab7fc..a3c1c5e1b7c 100644 --- a/spec/javascripts/monitoring/prometheus_graph_spec.js +++ b/spec/javascripts/monitoring/prometheus_graph_spec.js @@ -1,11 +1,8 @@ import 'jquery'; -import es6Promise from 'es6-promise'; import '~/lib/utils/common_utils'; import PrometheusGraph from '~/monitoring/prometheus_graph'; import { prometheusMockData } from './prometheus_mock_data'; -es6Promise.polyfill(); - describe('PrometheusGraph', () => { const fixtureName = 'static/environments/metrics.html.raw'; const prometheusGraphContainer = '.prometheus-graph'; diff --git a/spec/javascripts/polyfills/element_spec.js b/spec/javascripts/polyfills/element_spec.js new file mode 100644 index 00000000000..ecaaf1907ea --- /dev/null +++ b/spec/javascripts/polyfills/element_spec.js @@ -0,0 +1,36 @@ +import '~/commons/polyfills/element'; + +describe('Element polyfills', function () { + beforeEach(() => { + this.element = document.createElement('ul'); + }); + + describe('matches', () => { + it('returns true if element matches the selector', () => { + expect(this.element.matches('ul')).toBeTruthy(); + }); + + it("returns false if element doesn't match the selector", () => { + expect(this.element.matches('.not-an-element')).toBeFalsy(); + }); + }); + + describe('closest', () => { + beforeEach(() => { + this.childElement = document.createElement('li'); + this.element.appendChild(this.childElement); + }); + + it('returns the closest parent that matches the selector', () => { + expect(this.childElement.closest('ul').toString()).toBe(this.element.toString()); + }); + + it('returns itself if it matches the selector', () => { + expect(this.childElement.closest('li').toString()).toBe(this.childElement.toString()); + }); + + it('returns undefined if nothing matches the selector', () => { + expect(this.childElement.closest('.no-an-element')).toBeFalsy(); + }); + }); +}); diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index 4ac7e911740..285b7940174 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -1,8 +1,8 @@ /* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-return-assign, new-cap, vars-on-top, max-len */ /* global Sidebar */ -require('~/right_sidebar'); -require('~/extensions/jquery.js'); +import '~/commons/bootstrap'; +import '~/right_sidebar'; (function() { var $aside, $icon, $labelsIcon, $page, $toggle, assertSidebarState; diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index ffff643e371..9e19dabd0e3 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -31,13 +31,9 @@ require('~/shortcuts_issuable'); this.shortcut.replyWithSelectedText(); expect($(this.selector).val()).toBe(''); }); - it('triggers `input`', function() { - var focused = false; - $(this.selector).on('focus', function() { - focused = true; - }); + it('triggers `focus`', function() { this.shortcut.replyWithSelectedText(); - expect(focused).toBe(true); + expect(document.activeElement).toBe(document.querySelector(this.selector)); }); }); describe('with any selection', function() { diff --git a/yarn.lock b/yarn.lock index 55b8f1566ee..391b1c7eccf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1213,7 +1213,7 @@ cookie@0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" -core-js@^2.2.0, core-js@^2.4.0: +core-js@^2.2.0, core-js@^2.4.0, core-js@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" @@ -1553,7 +1553,7 @@ es6-map@^0.1.3: es6-symbol "~3.1.0" event-emitter "~0.3.4" -es6-promise@^4.0.5, es6-promise@~4.0.3: +es6-promise@~4.0.3: version "4.0.5" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.0.5.tgz#7882f30adde5b240ccfa7f7d78c548330951ae42" @@ -4123,14 +4123,6 @@ string-width@^2.0.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^3.0.0" -string.fromcodepoint@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/string.fromcodepoint/-/string.fromcodepoint-0.2.1.tgz#8d978333c0bc92538f50f383e4888f3e5619d653" - -string.prototype.codepointat@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/string.prototype.codepointat/-/string.prototype.codepointat-0.2.0.tgz#6b26e9bd3afcaa7be3b4269b526de1b82000ac78" - string_decoder@^0.10.25, string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" -- cgit v1.2.1 From 0f9e3e2b58f172590d7e8664ebc16b6d143b588c Mon Sep 17 00:00:00 2001 From: blackst0ne Date: Tue, 14 Mar 2017 09:13:03 +1100 Subject: Add quick submit for snippet forms --- app/views/shared/snippets/_form.html.haml | 2 +- changelogs/unreleased/add_quick_submit_for_snippets_form.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/add_quick_submit_for_snippets_form.yml diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml index e7f7db73223..0296597b294 100644 --- a/app/views/shared/snippets/_form.html.haml +++ b/app/views/shared/snippets/_form.html.haml @@ -3,7 +3,7 @@ = page_specific_javascript_bundle_tag('snippet') .snippet-form-holder - = form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input" } do |f| + = form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input js-quick-submit" } do |f| = form_errors(@snippet) .form-group diff --git a/changelogs/unreleased/add_quick_submit_for_snippets_form.yml b/changelogs/unreleased/add_quick_submit_for_snippets_form.yml new file mode 100644 index 00000000000..088f1335796 --- /dev/null +++ b/changelogs/unreleased/add_quick_submit_for_snippets_form.yml @@ -0,0 +1,4 @@ +--- +title: Add quick submit for snippet forms +merge_request: 9911 +author: blackst0ne -- cgit v1.2.1 From e10810ded8457e45921934359bc1e4fcb35fa785 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 13 Mar 2017 17:46:21 -0500 Subject: Fix missing blob line permalink updater on blob:show See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9461#note_25288831 --- app/assets/javascripts/dispatcher.js | 44 +++++++++++++++++------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index f0967d4f470..6d8174e199e 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -67,6 +67,25 @@ const UserCallout = require('./user_callout'); } path = page.split(':'); shortcut_handler = null; + + function initBlob() { + new LineHighlighter(); + + new BlobLinePermalinkUpdater( + document.querySelector('#blob-content-holder'), + '.diff-line-num[data-line-number]', + document.querySelectorAll('.js-data-file-blob-permalink-url, .js-blob-blame-link'), + ); + + shortcut_handler = new ShortcutsNavigation(); + fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url'); + fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href'); + new ShortcutsBlob({ + skipResetBindings: true, + fileBlobPermalinkUrl, + }); + } + switch (page) { case 'sessions:new': new UsernameValidator(); @@ -259,34 +278,13 @@ const UserCallout = require('./user_callout'); break; case 'projects:blob:show': gl.TargetBranchDropDown.bootstrap(); - new LineHighlighter(); - shortcut_handler = new ShortcutsNavigation(); - fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url'); - fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href'); - new ShortcutsBlob({ - skipResetBindings: true, - fileBlobPermalinkUrl, - }); + initBlob(); break; case 'projects:blob:edit': gl.TargetBranchDropDown.bootstrap(); break; case 'projects:blame:show': - new LineHighlighter(); - - new BlobLinePermalinkUpdater( - document.querySelector('#blob-content-holder'), - '.diff-line-num[data-line-number]', - document.querySelectorAll('.js-data-file-blob-permalink-url, .js-blob-blame-link'), - ); - - shortcut_handler = new ShortcutsNavigation(); - fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url'); - fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href'); - new ShortcutsBlob({ - skipResetBindings: true, - fileBlobPermalinkUrl, - }); + initBlob(); break; case 'groups:labels:new': case 'groups:labels:edit': -- cgit v1.2.1 From 5111525a4a1dc6a4e5498875b9198c265d6ea288 Mon Sep 17 00:00:00 2001 From: Lee Matos Date: Mon, 13 Mar 2017 23:18:45 +0000 Subject: Update markdown.md example with asterisks and underscores for clarity --- doc/user/markdown.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/user/markdown.md b/doc/user/markdown.md index db06224bac2..97de428d11d 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -431,7 +431,7 @@ Emphasis, aka italics, with *asterisks* or _underscores_. Strong emphasis, aka bold, with **asterisks** or __underscores__. -Combined emphasis with **asterisks and _underscores_**. +Combined emphasis with **_asterisks and underscores_**. Strikethrough uses two tildes. ~~Scratch this.~~ ``` -- cgit v1.2.1 From db59e735ae9c30bfa1e9d0800b6edfaaf6981f2a Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Tue, 28 Feb 2017 21:59:55 -0500 Subject: Toggle project name if too long --- app/assets/javascripts/dispatcher.js | 5 +++ app/assets/javascripts/group_name.js | 40 ++++++++++++++++++++ app/assets/stylesheets/framework/header.scss | 22 +++++++++++ app/helpers/groups_helper.rb | 11 +++--- app/views/layouts/header/_default.html.haml | 2 +- ...187-project-name-cut-off-with-nested-groups.yml | 4 ++ spec/features/groups/group_name_toggle.rb | 44 ++++++++++++++++++++++ 7 files changed, 122 insertions(+), 6 deletions(-) create mode 100644 app/assets/javascripts/group_name.js create mode 100644 changelogs/unreleased/28187-project-name-cut-off-with-nested-groups.yml create mode 100644 spec/features/groups/group_name_toggle.rb diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 7b9b9123c31..fcf3cb05e63 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -37,6 +37,7 @@ import PrometheusGraph from './monitoring/prometheus_graph'; // TODO: Maybe Make import Issue from './issue'; import BindInOut from './behaviors/bind_in_out'; +import GroupName from './group_name'; import GroupsList from './groups_list'; import ProjectsList from './projects_list'; import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; @@ -342,6 +343,9 @@ const UserCallout = require('./user_callout'); shortcut_handler = new ShortcutsDashboardNavigation(); new UserCallout(); break; + case 'groups': + new GroupName(); + break; case 'profiles': new NotificationsForm(); new NotificationsDropdown(); @@ -349,6 +353,7 @@ const UserCallout = require('./user_callout'); case 'projects': new Project(); new ProjectAvatar(); + new GroupName(); switch (path[1]) { case 'compare': new CompareAutocomplete(); diff --git a/app/assets/javascripts/group_name.js b/app/assets/javascripts/group_name.js new file mode 100644 index 00000000000..6a028f299b1 --- /dev/null +++ b/app/assets/javascripts/group_name.js @@ -0,0 +1,40 @@ +const GROUP_LIMIT = 2; + +export default class GroupName { + constructor() { + this.titleContainer = document.querySelector('.title'); + this.groups = document.querySelectorAll('.group-path'); + this.groupTitle = document.querySelector('.group-title'); + this.toggle = null; + this.isHidden = false; + this.init(); + } + + init() { + if (this.groups.length > GROUP_LIMIT) { + this.groups[this.groups.length - 1].classList.remove('hidable'); + this.addToggle(); + } + this.render(); + } + + addToggle() { + const header = document.querySelector('.header-content'); + this.toggle = document.createElement('button'); + this.toggle.className = 'text-expander group-name-toggle'; + this.toggle.setAttribute('aria-label', 'Toggle full path'); + this.toggle.innerHTML = '...'; + this.toggle.addEventListener('click', this.toggleGroups.bind(this)); + header.insertBefore(this.toggle, this.titleContainer); + this.toggleGroups(); + } + + toggleGroups() { + this.isHidden = !this.isHidden; + this.groupTitle.classList.toggle('is-hidden'); + } + + render() { + this.titleContainer.classList.remove('initializing'); + } +} diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 5d1aba4e529..6660a022260 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -164,11 +164,25 @@ header { } } + .group-name-toggle { + margin: 0 5px; + vertical-align: sub; + } + + .group-title { + &.is-hidden { + .hidable:not(:last-of-type) { + display: none; + } + } + } + .title { position: relative; padding-right: 20px; margin: 0; font-size: 18px; + max-width: 385px; display: inline-block; line-height: $header-height; font-weight: normal; @@ -178,6 +192,14 @@ header { vertical-align: top; white-space: nowrap; + &.initializing { + display: none; + } + + @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { + max-width: 300px; + } + @media (max-width: $screen-xs-max) { max-width: 190px; } diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 926c9703628..a6014088e92 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -12,17 +12,18 @@ module GroupsHelper end def group_title(group, name = nil, url = nil) + @has_group_title = true full_title = '' group.ancestors.each do |parent| - full_title += link_to(simple_sanitize(parent.name), group_path(parent)) - full_title += ' / '.html_safe + full_title += link_to(simple_sanitize(parent.name), group_path(parent), class: 'group-path hidable') + full_title += ' / '.html_safe end - full_title += link_to(simple_sanitize(group.name), group_path(group)) - full_title += ' · '.html_safe + link_to(simple_sanitize(name), url) if name + full_title += link_to(simple_sanitize(group.name), group_path(group), class: 'group-path') + full_title += ' · '.html_safe + link_to(simple_sanitize(name), url, class: 'group-path') if name - content_tag :span do + content_tag :span, class: 'group-title' do full_title.html_safe end end diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 6f4f2dbea3a..5fde5c2613e 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -67,7 +67,7 @@ = link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do = brand_header_logo - %h1.title= title + %h1.title{ class: ('initializing' if @has_group_title) }= title = yield :header_content diff --git a/changelogs/unreleased/28187-project-name-cut-off-with-nested-groups.yml b/changelogs/unreleased/28187-project-name-cut-off-with-nested-groups.yml new file mode 100644 index 00000000000..feca38ff083 --- /dev/null +++ b/changelogs/unreleased/28187-project-name-cut-off-with-nested-groups.yml @@ -0,0 +1,4 @@ +--- +title: Use toggle button to expand / collapse mulit-nested groups +merge_request: 9501 +author: diff --git a/spec/features/groups/group_name_toggle.rb b/spec/features/groups/group_name_toggle.rb new file mode 100644 index 00000000000..ada4ac66e04 --- /dev/null +++ b/spec/features/groups/group_name_toggle.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +feature 'Group name toggle', js: true do + let(:group) { create(:group) } + let(:nested_group_1) { create(:group, parent: group) } + let(:nested_group_2) { create(:group, parent: nested_group_1) } + let(:nested_group_3) { create(:group, parent: nested_group_2) } + + before do + login_as :user + end + + it 'is not present for less than 3 groups' do + visit group_path(group) + expect(page).not_to have_css('.group-name-toggle') + + visit group_path(nested_group_1) + expect(page).not_to have_css('.group-name-toggle') + end + + it 'is present for nested group of 3 or more in the namespace' do + visit group_path(nested_group_2) + expect(page).to have_css('.group-name-toggle') + + visit group_path(nested_group_3) + expect(page).to have_css('.group-name-toggle') + end + + context 'for group with at least 3 groups' do + before do + visit group_path(nested_group_2) + end + + it 'should show the full group namespace when toggled' do + expect(page).not_to have_content(group.name) + expect(page).to have_css('.group-path.hidable', visible: false) + + click_button '...' + + expect(page).to have_content(group.name) + expect(page).to have_css('.group-path.hidable', visible: true) + end + end +end -- cgit v1.2.1 From d48dda3c2ddbd260a828a1145f22834846353bc5 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Tue, 14 Mar 2017 00:52:23 +0000 Subject: Fix 'ExecJS disabled' error on issues index Occurred in production when an issue had an associated MR --- app/views/shared/_issuable_meta_data.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/_issuable_meta_data.html.haml b/app/views/shared/_issuable_meta_data.html.haml index 66310da5cd6..1d4fd71522d 100644 --- a/app/views/shared/_issuable_meta_data.html.haml +++ b/app/views/shared/_issuable_meta_data.html.haml @@ -6,7 +6,7 @@ - if issuable_mr > 0 %li - = image_tag('icon-merge-request-unmerged', class: 'icon-merge-request-unmerged') + = image_tag('icon-merge-request-unmerged.svg', class: 'icon-merge-request-unmerged') = issuable_mr - if upvotes > 0 -- cgit v1.2.1 From cbf1b656a464e0e544f7e559efed6851616e377f Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Tue, 14 Mar 2017 00:58:26 +0000 Subject: Use a button and a post request instead of UJS links - part 1 - Environments --- .../environments/components/environment.js | 76 ++++++++++++---------- .../environments/components/environment_actions.js | 56 ++++++++++++---- .../components/environment_external_url.js | 14 ++-- .../environments/components/environment_item.js | 34 ++++++---- .../components/environment_rollback.js | 49 ++++++++++++-- .../environments/components/environment_stop.js | 50 +++++++++++--- .../components/environment_terminal_button.js | 9 +-- .../environments/components/environments_table.js | 16 +++-- .../environments/environments_bundle.js | 2 +- app/assets/javascripts/environments/event_hub.js | 3 + .../folder/environments_folder_bundle.js | 2 +- .../folder/environments_folder_view.js | 19 +++--- .../environments/services/environments_service.js | 15 +++-- .../environments/stores/environments_store.js | 7 +- .../vue_shared/vue_resource_interceptor.js | 4 -- app/assets/stylesheets/pages/environments.scss | 8 +++ .../projects/environments/environments_spec.rb | 10 +-- .../environments/environment_actions_spec.js | 35 ++++++---- .../environments/environment_external_url_spec.js | 11 ++-- .../environments/environment_item_spec.js | 18 ++--- .../environments/environment_rollback_spec.js | 42 +++++++----- spec/javascripts/environments/environment_spec.js | 8 +-- .../environments/environment_stop_spec.js | 30 +++++---- .../environments/environment_table_spec.js | 8 ++- .../environment_terminal_button_spec.js | 24 +++++++ .../environments/environments_store_spec.js | 4 +- .../folder/environments_folder_view_spec.js | 8 +-- spec/javascripts/environments/mock_data.js | 12 +--- 28 files changed, 375 insertions(+), 199 deletions(-) create mode 100644 app/assets/javascripts/environments/event_hub.js create mode 100644 spec/javascripts/environments/environment_terminal_button_spec.js diff --git a/app/assets/javascripts/environments/components/environment.js b/app/assets/javascripts/environments/components/environment.js index 2cb48dde628..0923ce6b550 100644 --- a/app/assets/javascripts/environments/components/environment.js +++ b/app/assets/javascripts/environments/components/environment.js @@ -1,16 +1,17 @@ /* eslint-disable no-param-reassign, no-new */ /* global Flash */ +import EnvironmentsService from '../services/environments_service'; +import EnvironmentTable from './environments_table'; +import EnvironmentsStore from '../stores/environments_store'; +import eventHub from '../event_hub'; const Vue = window.Vue = require('vue'); window.Vue.use(require('vue-resource')); -const EnvironmentsService = require('../services/environments_service'); -const EnvironmentTable = require('./environments_table'); -const EnvironmentsStore = require('../stores/environments_store'); require('../../vue_shared/components/table_pagination'); require('../../lib/utils/common_utils'); require('../../vue_shared/vue_resource_interceptor'); -module.exports = Vue.component('environment-component', { +export default Vue.component('environment-component', { components: { 'environment-table': EnvironmentTable, @@ -66,33 +67,15 @@ module.exports = Vue.component('environment-component', { * Toggles loading property. */ created() { - const scope = gl.utils.getParameterByName('scope') || this.visibility; - const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber; - - const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`; - - const service = new EnvironmentsService(endpoint); - - this.isLoading = true; - - return service.get() - .then(resp => ({ - headers: resp.headers, - body: resp.json(), - })) - .then((response) => { - this.store.storeAvailableCount(response.body.available_count); - this.store.storeStoppedCount(response.body.stopped_count); - this.store.storeEnvironments(response.body.environments); - this.store.setPagination(response.headers); - }) - .then(() => { - this.isLoading = false; - }) - .catch(() => { - this.isLoading = false; - new Flash('An error occurred while fetching the environments.', 'alert'); - }); + this.service = new EnvironmentsService(this.endpoint); + + this.fetchEnvironments(); + + eventHub.$on('refreshEnvironments', this.fetchEnvironments); + }, + + beforeDestroyed() { + eventHub.$off('refreshEnvironments'); }, methods: { @@ -112,6 +95,32 @@ module.exports = Vue.component('environment-component', { gl.utils.visitUrl(param); return param; }, + + fetchEnvironments() { + const scope = gl.utils.getParameterByName('scope') || this.visibility; + const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber; + + this.isLoading = true; + + return this.service.get(scope, pageNumber) + .then(resp => ({ + headers: resp.headers, + body: resp.json(), + })) + .then((response) => { + this.store.storeAvailableCount(response.body.available_count); + this.store.storeStoppedCount(response.body.stopped_count); + this.store.storeEnvironments(response.body.environments); + this.store.setPagination(response.headers); + }) + .then(() => { + this.isLoading = false; + }) + .catch(() => { + this.isLoading = false; + new Flash('An error occurred while fetching the environments.'); + }); + }, }, template: ` @@ -144,7 +153,7 @@ module.exports = Vue.component('environment-component', {
      - +
      + :can-read-environment="canReadEnvironmentParsed" + :service="service"/>
      [], }, + + service: { + type: Object, + required: true, + }, }, data() { - return { playIconSvg }; + return { + playIconSvg, + isLoading: false, + }; + }, + + methods: { + onClickAction(endpoint) { + this.isLoading = true; + + this.service.postAction(endpoint) + .then(() => { + this.isLoading = false; + eventHub.$emit('refreshEnvironments'); + }) + .catch(() => { + this.isLoading = false; + new Flash('An error occured while making the request.'); + }); + }, }, template: `
    `, -}); +}; diff --git a/app/assets/javascripts/environments/components/environment_external_url.js b/app/assets/javascripts/environments/components/environment_external_url.js index 2599bba3c59..a554998f52c 100644 --- a/app/assets/javascripts/environments/components/environment_external_url.js +++ b/app/assets/javascripts/environments/components/environment_external_url.js @@ -1,9 +1,7 @@ /** * Renders the external url link in environments table. */ -const Vue = require('vue'); - -module.exports = Vue.component('external-url-component', { +export default { props: { externalUrl: { type: String, @@ -12,8 +10,12 @@ module.exports = Vue.component('external-url-component', { }, template: ` - - + + `, -}); +}; diff --git a/app/assets/javascripts/environments/components/environment_item.js b/app/assets/javascripts/environments/components/environment_item.js index 7f4e070b229..93919d41c60 100644 --- a/app/assets/javascripts/environments/components/environment_item.js +++ b/app/assets/javascripts/environments/components/environment_item.js @@ -1,13 +1,11 @@ -const Vue = require('vue'); -const Timeago = require('timeago.js'); - -require('../../lib/utils/text_utility'); -require('../../vue_shared/components/commit'); -const ActionsComponent = require('./environment_actions'); -const ExternalUrlComponent = require('./environment_external_url'); -const StopComponent = require('./environment_stop'); -const RollbackComponent = require('./environment_rollback'); -const TerminalButtonComponent = require('./environment_terminal_button'); +import Timeago from 'timeago.js'; +import ActionsComponent from './environment_actions'; +import ExternalUrlComponent from './environment_external_url'; +import StopComponent from './environment_stop'; +import RollbackComponent from './environment_rollback'; +import TerminalButtonComponent from './environment_terminal_button'; +import '../../lib/utils/text_utility'; +import '../../vue_shared/components/commit'; /** * Envrionment Item Component @@ -17,7 +15,7 @@ const TerminalButtonComponent = require('./environment_terminal_button'); const timeagoInstance = new Timeago(); -module.exports = Vue.component('environment-item', { +export default { components: { 'commit-component': gl.CommitComponent, @@ -46,6 +44,11 @@ module.exports = Vue.component('environment-item', { required: false, default: false, }, + + service: { + type: Object, + required: true, + }, }, computed: { @@ -489,22 +492,25 @@ module.exports = Vue.component('environment-item', {
    + :stop-url="model.stop_path" + :service="service"/> + :retry-url="retryUrl" + :service="service"/>
    `, -}); +}; diff --git a/app/assets/javascripts/environments/components/environment_rollback.js b/app/assets/javascripts/environments/components/environment_rollback.js index daf126eb4e8..baa15d9e5b5 100644 --- a/app/assets/javascripts/environments/components/environment_rollback.js +++ b/app/assets/javascripts/environments/components/environment_rollback.js @@ -1,10 +1,14 @@ +/* global Flash */ +/* eslint-disable no-new */ /** * Renders Rollback or Re deploy button in environments table depending - * of the provided property `isLastDeployment` + * of the provided property `isLastDeployment`. + * + * Makes a post request when the button is clicked. */ -const Vue = require('vue'); +import eventHub from '../event_hub'; -module.exports = Vue.component('rollback-component', { +export default { props: { retryUrl: { type: String, @@ -15,16 +19,49 @@ module.exports = Vue.component('rollback-component', { type: Boolean, default: true, }, + + service: { + type: Object, + required: true, + }, + }, + + data() { + return { + isLoading: false, + }; + }, + + methods: { + onClick() { + this.isLoading = true; + + this.service.postAction(this.retryUrl) + .then(() => { + this.isLoading = false; + eventHub.$emit('refreshEnvironments'); + }) + .catch(() => { + this.isLoading = false; + new Flash('An error occured while making the request.'); + }); + }, }, template: ` - + `, -}); +}; diff --git a/app/assets/javascripts/environments/components/environment_stop.js b/app/assets/javascripts/environments/components/environment_stop.js index 96983a19568..5404d647745 100644 --- a/app/assets/javascripts/environments/components/environment_stop.js +++ b/app/assets/javascripts/environments/components/environment_stop.js @@ -1,24 +1,56 @@ +/* global Flash */ +/* eslint-disable no-new, no-alert */ /** * Renders the stop "button" that allows stop an environment. * Used in environments table. */ -const Vue = require('vue'); +import eventHub from '../event_hub'; -module.exports = Vue.component('stop-component', { +export default { props: { stopUrl: { type: String, default: '', }, + + service: { + type: Object, + required: true, + }, + }, + + data() { + return { + isLoading: false, + }; + }, + + methods: { + onClick() { + if (confirm('Are you sure you want to stop this environment?')) { + this.isLoading = true; + + this.service.postAction(this.retryUrl) + .then(() => { + this.isLoading = false; + eventHub.$emit('refreshEnvironments'); + }) + .catch(() => { + this.isLoading = false; + new Flash('An error occured while making the request.', 'alert'); + }); + } + }, }, template: ` - + `, -}); +}; diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.js b/app/assets/javascripts/environments/components/environment_terminal_button.js index e86607e78f4..66a71faa02f 100644 --- a/app/assets/javascripts/environments/components/environment_terminal_button.js +++ b/app/assets/javascripts/environments/components/environment_terminal_button.js @@ -2,13 +2,13 @@ * Renders a terminal button to open a web terminal. * Used in environments table. */ -const Vue = require('vue'); -const terminalIconSvg = require('icons/_icon_terminal.svg'); +import terminalIconSvg from 'icons/_icon_terminal.svg'; -module.exports = Vue.component('terminal-button-component', { +export default { props: { terminalPath: { type: String, + required: false, default: '', }, }, @@ -19,8 +19,9 @@ module.exports = Vue.component('terminal-button-component', { template: ` ${terminalIconSvg} `, -}); +}; diff --git a/app/assets/javascripts/environments/components/environments_table.js b/app/assets/javascripts/environments/components/environments_table.js index 4088d63be80..5f07b612b91 100644 --- a/app/assets/javascripts/environments/components/environments_table.js +++ b/app/assets/javascripts/environments/components/environments_table.js @@ -1,11 +1,9 @@ /** * Render environments table. */ -const Vue = require('vue'); -const EnvironmentItem = require('./environment_item'); - -module.exports = Vue.component('environment-table-component', { +import EnvironmentItem from './environment_item'; +export default { components: { 'environment-item': EnvironmentItem, }, @@ -28,6 +26,11 @@ module.exports = Vue.component('environment-table-component', { required: false, default: false, }, + + service: { + type: Object, + required: true, + }, }, template: ` @@ -48,9 +51,10 @@ module.exports = Vue.component('environment-table-component', { + :can-read-environment="canReadEnvironment" + :service="service"> `, -}); +}; diff --git a/app/assets/javascripts/environments/environments_bundle.js b/app/assets/javascripts/environments/environments_bundle.js index 7bbba91bc10..8d963b335cf 100644 --- a/app/assets/javascripts/environments/environments_bundle.js +++ b/app/assets/javascripts/environments/environments_bundle.js @@ -1,4 +1,4 @@ -const EnvironmentsComponent = require('./components/environment'); +import EnvironmentsComponent from './components/environment'; $(() => { window.gl = window.gl || {}; diff --git a/app/assets/javascripts/environments/event_hub.js b/app/assets/javascripts/environments/event_hub.js new file mode 100644 index 00000000000..0948c2e5352 --- /dev/null +++ b/app/assets/javascripts/environments/event_hub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js index d2ca465351a..f939eccf246 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js +++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js @@ -1,4 +1,4 @@ -const EnvironmentsFolderComponent = require('./environments_folder_view'); +import EnvironmentsFolderComponent from './environments_folder_view'; $(() => { window.gl = window.gl || {}; diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.js b/app/assets/javascripts/environments/folder/environments_folder_view.js index 2a9d0492d7a..7abcf6dbbea 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_view.js +++ b/app/assets/javascripts/environments/folder/environments_folder_view.js @@ -1,16 +1,16 @@ /* eslint-disable no-param-reassign, no-new */ /* global Flash */ +import EnvironmentsService from '../services/environments_service'; +import EnvironmentTable from '../components/environments_table'; +import EnvironmentsStore from '../stores/environments_store'; const Vue = window.Vue = require('vue'); window.Vue.use(require('vue-resource')); -const EnvironmentsService = require('../services/environments_service'); -const EnvironmentTable = require('../components/environments_table'); -const EnvironmentsStore = require('../stores/environments_store'); require('../../vue_shared/components/table_pagination'); require('../../lib/utils/common_utils'); require('../../vue_shared/vue_resource_interceptor'); -module.exports = Vue.component('environment-folder-view', { +export default Vue.component('environment-folder-view', { components: { 'environment-table': EnvironmentTable, @@ -88,11 +88,11 @@ module.exports = Vue.component('environment-folder-view', { const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`; - const service = new EnvironmentsService(endpoint); + this.service = new EnvironmentsService(endpoint); this.isLoading = true; - return service.get() + return this.service.get() .then(resp => ({ headers: resp.headers, body: resp.json(), @@ -168,13 +168,12 @@ module.exports = Vue.component('environment-folder-view', { :can-read-environment="canReadEnvironmentParsed" :play-icon-svg="playIconSvg" :terminal-icon-svg="terminalIconSvg" - :commit-icon-svg="commitIconSvg"> - + :commit-icon-svg="commitIconSvg" + :service="service"/> - + :pageInfo="state.paginationInformation"/> diff --git a/app/assets/javascripts/environments/services/environments_service.js b/app/assets/javascripts/environments/services/environments_service.js index effc6c4c838..76296c83d11 100644 --- a/app/assets/javascripts/environments/services/environments_service.js +++ b/app/assets/javascripts/environments/services/environments_service.js @@ -1,13 +1,16 @@ -const Vue = require('vue'); +/* eslint-disable class-methods-use-this */ +import Vue from 'vue'; -class EnvironmentsService { +export default class EnvironmentsService { constructor(endpoint) { this.environments = Vue.resource(endpoint); } - get() { - return this.environments.get(); + get(scope, page) { + return this.environments.get({ scope, page }); } -} -module.exports = EnvironmentsService; + postAction(endpoint) { + return Vue.http.post(endpoint, {}, { emulateJSON: true }); + } +} diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js index 15cd9bde08e..d3fe3872c56 100644 --- a/app/assets/javascripts/environments/stores/environments_store.js +++ b/app/assets/javascripts/environments/stores/environments_store.js @@ -1,11 +1,12 @@ -require('~/lib/utils/common_utils'); +import '~/lib/utils/common_utils'; + /** * Environments Store. * * Stores received environments, count of stopped environments and count of * available environments. */ -class EnvironmentsStore { +export default class EnvironmentsStore { constructor() { this.state = {}; this.state.environments = []; @@ -86,5 +87,3 @@ class EnvironmentsStore { return count; } } - -module.exports = EnvironmentsStore; diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js index d3229f9f730..4157fefddc9 100644 --- a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js +++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js @@ -6,10 +6,6 @@ Vue.http.interceptors.push((request, next) => { Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1; next((response) => { - if (typeof response.data === 'string') { - response.data = JSON.parse(response.data); - } - Vue.activeResources--; }); }); diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 0e2b8dba780..73a5da715f2 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -141,6 +141,14 @@ margin-right: 0; } } + + .no-btn { + border: none; + background: none; + outline: none; + width: 100%; + text-align: left; + } } } diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb index 25f31b423b8..641e2cf7402 100644 --- a/spec/features/projects/environments/environments_spec.rb +++ b/spec/features/projects/environments/environments_spec.rb @@ -111,10 +111,8 @@ feature 'Environments page', :feature, :js do find('.js-dropdown-play-icon-container').click expect(page).to have_content(action.name.humanize) - expect { click_link(action.name.humanize) } + expect { find('.js-manual-action-link').click } .not_to change { Ci::Pipeline.count } - - expect(action.reload).to be_pending end scenario 'does show build name and id' do @@ -158,12 +156,6 @@ feature 'Environments page', :feature, :js do expect(page).to have_selector('.stop-env-link') end - scenario 'starts build when stop button clicked' do - find('.stop-env-link').click - - expect(page).to have_content('close_app') - end - context 'for reporter' do let(:role) { :reporter } diff --git a/spec/javascripts/environments/environment_actions_spec.js b/spec/javascripts/environments/environment_actions_spec.js index d50d45d295e..85b73f1d4e2 100644 --- a/spec/javascripts/environments/environment_actions_spec.js +++ b/spec/javascripts/environments/environment_actions_spec.js @@ -1,14 +1,16 @@ -const ActionsComponent = require('~/environments/components/environment_actions'); +import Vue from 'vue'; +import actionsComp from '~/environments/components/environment_actions'; describe('Actions Component', () => { - preloadFixtures('static/environments/element.html.raw'); + let ActionsComponent; + let actionsMock; + let spy; + let component; beforeEach(() => { - loadFixtures('static/environments/element.html.raw'); - }); + ActionsComponent = Vue.extend(actionsComp); - it('should render a dropdown with the provided actions', () => { - const actionsMock = [ + actionsMock = [ { name: 'bar', play_path: 'https://gitlab.com/play', @@ -19,18 +21,27 @@ describe('Actions Component', () => { }, ]; - const component = new ActionsComponent({ - el: document.querySelector('.test-dom-element'), + spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve()); + component = new ActionsComponent({ propsData: { actions: actionsMock, + service: { + postAction: spy, + }, }, - }); + }).$mount(); + }); + it('should render a dropdown with the provided actions', () => { expect( component.$el.querySelectorAll('.dropdown-menu li').length, ).toEqual(actionsMock.length); - expect( - component.$el.querySelector('.dropdown-menu li a').getAttribute('href'), - ).toEqual(actionsMock[0].play_path); + }); + + it('should call the service when an action is clicked', () => { + component.$el.querySelector('.dropdown').click(); + component.$el.querySelector('.js-manual-action-link').click(); + + expect(spy).toHaveBeenCalledWith(actionsMock[0].play_path); }); }); diff --git a/spec/javascripts/environments/environment_external_url_spec.js b/spec/javascripts/environments/environment_external_url_spec.js index 393dbb5aae0..9af218a27ff 100644 --- a/spec/javascripts/environments/environment_external_url_spec.js +++ b/spec/javascripts/environments/environment_external_url_spec.js @@ -1,19 +1,20 @@ -const ExternalUrlComponent = require('~/environments/components/environment_external_url'); +import Vue from 'vue'; +import externalUrlComp from '~/environments/components/environment_external_url'; describe('External URL Component', () => { - preloadFixtures('static/environments/element.html.raw'); + let ExternalUrlComponent; + beforeEach(() => { - loadFixtures('static/environments/element.html.raw'); + ExternalUrlComponent = Vue.extend(externalUrlComp); }); it('should link to the provided externalUrl prop', () => { const externalURL = 'https://gitlab.com'; const component = new ExternalUrlComponent({ - el: document.querySelector('.test-dom-element'), propsData: { externalUrl: externalURL, }, - }); + }).$mount(); expect(component.$el.getAttribute('href')).toEqual(externalURL); expect(component.$el.querySelector('fa-external-link')).toBeDefined(); diff --git a/spec/javascripts/environments/environment_item_spec.js b/spec/javascripts/environments/environment_item_spec.js index 7fea80ed799..4d42de4d549 100644 --- a/spec/javascripts/environments/environment_item_spec.js +++ b/spec/javascripts/environments/environment_item_spec.js @@ -1,10 +1,12 @@ -window.timeago = require('timeago.js'); -const EnvironmentItem = require('~/environments/components/environment_item'); +import 'timeago.js'; +import Vue from 'vue'; +import environmentItemComp from '~/environments/components/environment_item'; describe('Environment item', () => { - preloadFixtures('static/environments/table.html.raw'); + let EnvironmentItem; + beforeEach(() => { - loadFixtures('static/environments/table.html.raw'); + EnvironmentItem = Vue.extend(environmentItemComp); }); describe('When item is folder', () => { @@ -21,13 +23,13 @@ describe('Environment item', () => { }; component = new EnvironmentItem({ - el: document.querySelector('tr#environment-row'), propsData: { model: mockItem, canCreateDeployment: false, canReadEnvironment: true, + service: {}, }, - }); + }).$mount(); }); it('Should render folder icon and name', () => { @@ -109,13 +111,13 @@ describe('Environment item', () => { }; component = new EnvironmentItem({ - el: document.querySelector('tr#environment-row'), propsData: { model: environment, canCreateDeployment: true, canReadEnvironment: true, + service: {}, }, - }); + }).$mount(); }); it('should render environment name', () => { diff --git a/spec/javascripts/environments/environment_rollback_spec.js b/spec/javascripts/environments/environment_rollback_spec.js index 4a596baad09..7cb39d9df03 100644 --- a/spec/javascripts/environments/environment_rollback_spec.js +++ b/spec/javascripts/environments/environment_rollback_spec.js @@ -1,47 +1,59 @@ -const RollbackComponent = require('~/environments/components/environment_rollback'); +import Vue from 'vue'; +import rollbackComp from '~/environments/components/environment_rollback'; describe('Rollback Component', () => { - preloadFixtures('static/environments/element.html.raw'); - const retryURL = 'https://gitlab.com/retry'; + let RollbackComponent; + let spy; beforeEach(() => { - loadFixtures('static/environments/element.html.raw'); + RollbackComponent = Vue.extend(rollbackComp); + spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve()); }); - it('Should link to the provided retryUrl', () => { + it('Should render Re-deploy label when isLastDeployment is true', () => { const component = new RollbackComponent({ el: document.querySelector('.test-dom-element'), propsData: { retryUrl: retryURL, isLastDeployment: true, + service: { + postAction: spy, + }, }, - }); + }).$mount(); - expect(component.$el.getAttribute('href')).toEqual(retryURL); + expect(component.$el.querySelector('span').textContent).toContain('Re-deploy'); }); - it('Should render Re-deploy label when isLastDeployment is true', () => { + it('Should render Rollback label when isLastDeployment is false', () => { const component = new RollbackComponent({ el: document.querySelector('.test-dom-element'), propsData: { retryUrl: retryURL, - isLastDeployment: true, + isLastDeployment: false, + service: { + postAction: spy, + }, }, - }); + }).$mount(); - expect(component.$el.querySelector('span').textContent).toContain('Re-deploy'); + expect(component.$el.querySelector('span').textContent).toContain('Rollback'); }); - it('Should render Rollback label when isLastDeployment is false', () => { + it('should call the service when the button is clicked', () => { const component = new RollbackComponent({ - el: document.querySelector('.test-dom-element'), propsData: { retryUrl: retryURL, isLastDeployment: false, + service: { + postAction: spy, + }, }, - }); + }).$mount(); - expect(component.$el.querySelector('span').textContent).toContain('Rollback'); + component.$el.click(); + + expect(spy).toHaveBeenCalledWith(retryURL); }); }); diff --git a/spec/javascripts/environments/environment_spec.js b/spec/javascripts/environments/environment_spec.js index edd0cad32d0..9601575577e 100644 --- a/spec/javascripts/environments/environment_spec.js +++ b/spec/javascripts/environments/environment_spec.js @@ -1,7 +1,7 @@ -const Vue = require('vue'); -require('~/flash'); -const EnvironmentsComponent = require('~/environments/components/environment'); -const { environment } = require('./mock_data'); +import Vue from 'vue'; +import '~/flash'; +import EnvironmentsComponent from '~/environments/components/environment'; +import { environment } from './mock_data'; describe('Environment', () => { preloadFixtures('static/environments/environments.html.raw'); diff --git a/spec/javascripts/environments/environment_stop_spec.js b/spec/javascripts/environments/environment_stop_spec.js index 5ca65b1debc..8f79b88f3df 100644 --- a/spec/javascripts/environments/environment_stop_spec.js +++ b/spec/javascripts/environments/environment_stop_spec.js @@ -1,28 +1,34 @@ -const StopComponent = require('~/environments/components/environment_stop'); +import Vue from 'vue'; +import stopComp from '~/environments/components/environment_stop'; describe('Stop Component', () => { - preloadFixtures('static/environments/element.html.raw'); - - let stopURL; + let StopComponent; let component; + let spy; + const stopURL = '/stop'; beforeEach(() => { - loadFixtures('static/environments/element.html.raw'); + StopComponent = Vue.extend(stopComp); + spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve()); + spyOn(window, 'confirm').and.returnValue(true); - stopURL = '/stop'; component = new StopComponent({ - el: document.querySelector('.test-dom-element'), propsData: { stopUrl: stopURL, + service: { + postAction: spy, + }, }, - }); + }).$mount(); }); - it('should link to the provided URL', () => { - expect(component.$el.getAttribute('href')).toEqual(stopURL); + it('should render a button to stop the environment', () => { + expect(component.$el.tagName).toEqual('BUTTON'); + expect(component.$el.getAttribute('title')).toEqual('Stop Environment'); }); - it('should have a data-confirm attribute', () => { - expect(component.$el.getAttribute('data-confirm')).toEqual('Are you sure you want to stop this environment?'); + it('should call the service when an action is clicked', () => { + component.$el.click(); + expect(spy).toHaveBeenCalled(); }); }); diff --git a/spec/javascripts/environments/environment_table_spec.js b/spec/javascripts/environments/environment_table_spec.js index be4330b5012..3df967848a7 100644 --- a/spec/javascripts/environments/environment_table_spec.js +++ b/spec/javascripts/environments/environment_table_spec.js @@ -1,4 +1,5 @@ -const EnvironmentTable = require('~/environments/components/environments_table'); +import Vue from 'vue'; +import environmentTableComp from '~/environments/components/environments_table'; describe('Environment item', () => { preloadFixtures('static/environments/element.html.raw'); @@ -16,14 +17,17 @@ describe('Environment item', () => { }, }; + const EnvironmentTable = Vue.extend(environmentTableComp); + const component = new EnvironmentTable({ el: document.querySelector('.test-dom-element'), propsData: { environments: [{ mockItem }], canCreateDeployment: false, canReadEnvironment: true, + service: {}, }, - }); + }).$mount(); expect(component.$el.tagName).toEqual('TABLE'); }); diff --git a/spec/javascripts/environments/environment_terminal_button_spec.js b/spec/javascripts/environments/environment_terminal_button_spec.js new file mode 100644 index 00000000000..b07aa4e1745 --- /dev/null +++ b/spec/javascripts/environments/environment_terminal_button_spec.js @@ -0,0 +1,24 @@ +import Vue from 'vue'; +import terminalComp from '~/environments/components/environment_terminal_button'; + +describe('Stop Component', () => { + let TerminalComponent; + let component; + const terminalPath = '/path'; + + beforeEach(() => { + TerminalComponent = Vue.extend(terminalComp); + + component = new TerminalComponent({ + propsData: { + terminalPath, + }, + }).$mount(); + }); + + it('should render a link to open a web terminal with the provided path', () => { + expect(component.$el.tagName).toEqual('A'); + expect(component.$el.getAttribute('title')).toEqual('Open web terminal'); + expect(component.$el.getAttribute('href')).toEqual(terminalPath); + }); +}); diff --git a/spec/javascripts/environments/environments_store_spec.js b/spec/javascripts/environments/environments_store_spec.js index 77e182b3830..115d84b50f5 100644 --- a/spec/javascripts/environments/environments_store_spec.js +++ b/spec/javascripts/environments/environments_store_spec.js @@ -1,5 +1,5 @@ -const Store = require('~/environments/stores/environments_store'); -const { environmentsList, serverData } = require('./mock_data'); +import Store from '~/environments/stores/environments_store'; +import { environmentsList, serverData } from './mock_data'; (() => { describe('Store', () => { diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js index d1335b5b304..43a217a67f5 100644 --- a/spec/javascripts/environments/folder/environments_folder_view_spec.js +++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js @@ -1,7 +1,7 @@ -const Vue = require('vue'); -require('~/flash'); -const EnvironmentsFolderViewComponent = require('~/environments/folder/environments_folder_view'); -const { environmentsList } = require('../mock_data'); +import Vue from 'vue'; +import '~/flash'; +import EnvironmentsFolderViewComponent from '~/environments/folder/environments_folder_view'; +import { environmentsList } from '../mock_data'; describe('Environments Folder View', () => { preloadFixtures('static/environments/environments_folder_view.html.raw'); diff --git a/spec/javascripts/environments/mock_data.js b/spec/javascripts/environments/mock_data.js index 5c395c6b2d8..30861481cc5 100644 --- a/spec/javascripts/environments/mock_data.js +++ b/spec/javascripts/environments/mock_data.js @@ -1,4 +1,4 @@ -const environmentsList = [ +export const environmentsList = [ { name: 'DEV', size: 1, @@ -30,7 +30,7 @@ const environmentsList = [ }, ]; -const serverData = [ +export const serverData = [ { name: 'DEV', size: 1, @@ -67,7 +67,7 @@ const serverData = [ }, ]; -const environment = { +export const environment = { name: 'DEV', size: 1, latest: { @@ -84,9 +84,3 @@ const environment = { updated_at: '2017-01-31T10:53:46.894Z', }, }; - -module.exports = { - environmentsList, - environment, - serverData, -}; -- cgit v1.2.1 From 29c5b31c192925e2a73e789ab6904168801ab260 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 13 Mar 2017 22:16:29 -0700 Subject: Remove unused satellites config Note that the old migrations depend on 1_settings.rb, so we can't quite remove those completely. --- config/gitlab.yml.example | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 2bc39ea3f65..954809a882c 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -456,14 +456,6 @@ production: &base # 4. Advanced settings # ========================== - # GitLab Satellites - # - # Note for maintainers: keep the satellites.path setting until GitLab 9.0 at - # least. This setting is fed to 'rm -rf' in - # db/migrate/20151023144219_remove_satellites.rb - satellites: - path: /home/git/gitlab-satellites/ - ## Repositories settings repositories: # Paths where repositories can be stored. Give the canonicalized absolute pathname. @@ -581,8 +573,6 @@ test: # In order to setup it correctly you need to specify # your system username you use to run GitLab # user: YOUR_USERNAME - satellites: - path: tmp/tests/gitlab-satellites/ repositories: storages: default: -- cgit v1.2.1 From 0b20d850cb964da89f8b0df3c9e78a1cc2a86e1b Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 14 Mar 2017 14:36:33 +0800 Subject: Fix for postgresql --- db/migrate/20161201160452_migrate_project_statistics.rb | 4 ++-- db/migrate/20161212142807_add_lower_path_index_to_routes.rb | 2 +- db/migrate/20170130204620_add_index_to_project_authorizations.rb | 4 ++++ db/migrate/20170305203726_add_owner_id_foreign_key.rb | 6 +++++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/db/migrate/20161201160452_migrate_project_statistics.rb b/db/migrate/20161201160452_migrate_project_statistics.rb index 3ae3f2c159b..8386f7f9d4f 100644 --- a/db/migrate/20161201160452_migrate_project_statistics.rb +++ b/db/migrate/20161201160452_migrate_project_statistics.rb @@ -17,7 +17,7 @@ class MigrateProjectStatistics < ActiveRecord::Migration end def down - add_column_with_default :projects, :repository_size, :float, default: 0.0 - add_column_with_default :projects, :commit_count, :integer, default: 0 + add_column :projects, :repository_size, :float, default: 0.0 + add_column :projects, :commit_count, :integer, default: 0 end end diff --git a/db/migrate/20161212142807_add_lower_path_index_to_routes.rb b/db/migrate/20161212142807_add_lower_path_index_to_routes.rb index 6958500306f..53f4c6bbb18 100644 --- a/db/migrate/20161212142807_add_lower_path_index_to_routes.rb +++ b/db/migrate/20161212142807_add_lower_path_index_to_routes.rb @@ -17,6 +17,6 @@ class AddLowerPathIndexToRoutes < ActiveRecord::Migration def down return unless Gitlab::Database.postgresql? - remove_index :routes, name: :index_on_routes_lower_path + remove_index :routes, name: :index_on_routes_lower_path if index_exists?(:routes, name: :index_on_routes_lower_path) end end diff --git a/db/migrate/20170130204620_add_index_to_project_authorizations.rb b/db/migrate/20170130204620_add_index_to_project_authorizations.rb index e9a0aee4d6a..a8c504f265a 100644 --- a/db/migrate/20170130204620_add_index_to_project_authorizations.rb +++ b/db/migrate/20170130204620_add_index_to_project_authorizations.rb @@ -8,4 +8,8 @@ class AddIndexToProjectAuthorizations < ActiveRecord::Migration def up add_concurrent_index(:project_authorizations, :project_id) end + + def down + remove_index(:project_authorizations, :project_id) + end end diff --git a/db/migrate/20170305203726_add_owner_id_foreign_key.rb b/db/migrate/20170305203726_add_owner_id_foreign_key.rb index 3eece0e2eb5..5fbdc45f1a7 100644 --- a/db/migrate/20170305203726_add_owner_id_foreign_key.rb +++ b/db/migrate/20170305203726_add_owner_id_foreign_key.rb @@ -5,7 +5,11 @@ class AddOwnerIdForeignKey < ActiveRecord::Migration disable_ddl_transaction! - def change + def up add_concurrent_foreign_key :ci_triggers, :users, column: :owner_id, on_delete: :cascade end + + def down + remove_foreign_key :ci_triggers, column: :owner_id + end end -- cgit v1.2.1 From 77d93d33820b9771b906c2d2bd708b4b910aa9e9 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 14 Mar 2017 09:53:28 +0100 Subject: Add missing steps of Pages source installation [ci skip] --- doc/administration/pages/source.md | 82 ++++++++++++++++++++++++++++---------- 1 file changed, 62 insertions(+), 20 deletions(-) diff --git a/doc/administration/pages/source.md b/doc/administration/pages/source.md index f6f50e2c571..b4588f8b43c 100644 --- a/doc/administration/pages/source.md +++ b/doc/administration/pages/source.md @@ -1,5 +1,9 @@ # GitLab Pages administration for source installations +>**Note:** +Before attempting to enable GitLab Pages, first make sure you have +[installed GitLab](../../install/installation.md) successfully. + This is the documentation for configuring a GitLab Pages when you have installed GitLab from source and not using the Omnibus packages. @@ -13,7 +17,33 @@ Pages to the latest supported version. ## Overview -[Read the Omnibus overview section.](index.md#overview) +GitLab Pages makes use of the [GitLab Pages daemon], a simple HTTP server +written in Go that can listen on an external IP address and provide support for +custom domains and custom certificates. It supports dynamic certificates through +SNI and exposes pages using HTTP2 by default. +You are encouraged to read its [README][pages-readme] to fully understand how +it works. + +--- + +In the case of [custom domains](#custom-domains) (but not +[wildcard domains](#wildcard-domains)), the Pages daemon needs to listen on +ports `80` and/or `443`. For that reason, there is some flexibility in the way +which you can set it up: + +1. Run the Pages daemon in the same server as GitLab, listening on a secondary IP. +1. Run the Pages daemon in a separate server. In that case, the + [Pages path](#change-storage-path) must also be present in the server that + the Pages daemon is installed, so you will have to share it via network. +1. Run the Pages daemon in the same server as GitLab, listening on the same IP + but on different ports. In that case, you will have to proxy the traffic with + a loadbalancer. If you choose that route note that you should use TCP load + balancing for HTTPS. If you use TLS-termination (HTTPS-load balancing) the + pages will not be able to be served with user provided certificates. For + HTTP it's OK to use HTTP or TCP load balancing. + +In this document, we will proceed assuming the first option. If you are not +supporting custom domains a secondary IP is not needed. ## Prerequisites @@ -75,7 +105,7 @@ The Pages daemon doesn't listen to the outside world. cd /home/git sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git cd gitlab-pages - sudo -u git -H git checkout v0.2.4 + sudo -u git -H git checkout v0.3.2 sudo -u git -H make ``` @@ -100,14 +130,21 @@ The Pages daemon doesn't listen to the outside world. https: false ``` -1. Copy the `gitlab-pages-ssl` Nginx configuration file: +1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in + order to enable the pages daemon. In `gitlab_pages_options` the + `-pages-domain` must match the `host` setting that you set above. - ```bash - sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf - sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf ``` + gitlab_pages_enabled=true + gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 + ``` + +1. Copy the `gitlab-pages` Nginx configuration file: - Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. + ```bash + sudo cp lib/support/nginx/gitlab-pages /etc/nginx/sites-available/gitlab-pages.conf + sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages.conf + ``` 1. Restart NGINX 1. [Restart GitLab][restart] @@ -131,7 +168,7 @@ outside world. cd /home/git sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git cd gitlab-pages - sudo -u git -H git checkout v0.2.4 + sudo -u git -H git checkout v0.3.2 sudo -u git -H make ``` @@ -149,6 +186,17 @@ outside world. https: true ``` +1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in + order to enable the pages daemon. In `gitlab_pages_options` the + `-pages-domain` must match the `host` setting that you set above. + The `-root-cert` and `-root-key` settings are the wildcard TLS certificates + of the `example.io` domain: + + ``` + gitlab_pages_enabled=true + gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -root-cert /path/to/example.io.crt -root-key /path/to/example.io.key + ``` + 1. Copy the `gitlab-pages-ssl` Nginx configuration file: ```bash @@ -156,12 +204,9 @@ outside world. sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf ``` - Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. - 1. Restart NGINX 1. [Restart GitLab][restart] - ## Advanced configuration In addition to the wildcard domains, you can also have the option to configure @@ -189,7 +234,7 @@ world. Custom domains are supported, but no TLS. cd /home/git sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git cd gitlab-pages - sudo -u git -H git checkout v0.2.4 + sudo -u git -H git checkout v0.3.2 sudo -u git -H make ``` @@ -224,12 +269,10 @@ world. Custom domains are supported, but no TLS. 1. Copy the `gitlab-pages-ssl` Nginx configuration file: ```bash - sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf - sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf + sudo cp lib/support/nginx/gitlab-pages /etc/nginx/sites-available/gitlab-pages.conf + sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages.conf ``` - Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. - 1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace `0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab listens to. @@ -257,7 +300,7 @@ world. Custom domains and TLS are supported. cd /home/git sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git cd gitlab-pages - sudo -u git -H git checkout v0.2.4 + sudo -u git -H git checkout v0.3.2 sudo -u git -H make ``` @@ -300,8 +343,6 @@ world. Custom domains and TLS are supported. sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf ``` - Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. - 1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace `0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab listens to. @@ -392,5 +433,6 @@ than GitLab to prevent XSS attacks. [pages-userguide]: ../../user/project/pages/index.md [reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure [restart]: ../restart_gitlab.md#installations-from-source -[gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.2.4 +[gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.3.2 +[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/support/init.d/gitlab.default.example [shared runners]: ../../ci/runners/README.md -- cgit v1.2.1 From 4cbe05e8029c32e68719b4b8445c6ab5677c519d Mon Sep 17 00:00:00 2001 From: Xiaogang Wen Date: Tue, 14 Mar 2017 09:09:56 +0000 Subject: Patch 15 --- doc/integration/github.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/integration/github.md b/doc/integration/github.md index cea85f073cc..4b0d33334bd 100644 --- a/doc/integration/github.md +++ b/doc/integration/github.md @@ -19,7 +19,7 @@ GitHub will generate an application ID and secret key for you to use. - Application name: This can be anything. Consider something like `'s GitLab` or `'s GitLab` or something else descriptive. - Homepage URL: The URL to your GitLab installation. 'https://gitlab.company.com' - Application description: Fill this in if you wish. - - Authorization callback URL is 'http(s)://${YOUR_DOMAIN}' + - Authorization callback URL is 'http(s)://${YOUR_DOMAIN}'. Please make sure the port is included if your Gitlab instance is not configured on default port. 1. Select "Register application". 1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot). -- cgit v1.2.1 From e8c8ea8408de8241f003d76a25cdc773877e98e1 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 14 Mar 2017 10:28:11 +0100 Subject: Include instructions to update /etc/default/gitlab We were missing some info on updating /etc/default/gitlab In particular, changes needed to be made in order for Pages to work, see https://gitlab.com/gitlab-org/gitlab-ce/issues/29372 --- doc/update/8.16-to-8.17.md | 14 +++++++++++++- doc/update/8.17-to-9.0.md | 13 +++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/doc/update/8.16-to-8.17.md b/doc/update/8.16-to-8.17.md index 954109ba18f..74ffe0bc846 100644 --- a/doc/update/8.16-to-8.17.md +++ b/doc/update/8.16-to-8.17.md @@ -139,7 +139,7 @@ sudo -u git -H git checkout v4.1.1 #### New configuration options for `gitlab.yml` -There are new configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`: +There might be new configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`: ```sh cd /home/git/gitlab @@ -195,6 +195,16 @@ See [smtp_settings.rb.sample] as an example. #### Init script +There might be new configuration options available for [`gitlab.default.example`][gl-example]. +You need to update this file if you want to [enable GitLab Pages][pages-admin]. +View them with the command below and apply them manually to your current `/etc/default/gitlab`: + +```sh +cd /home/git/gitlab + +git diff origin/8-16-stable:lib/support/init.d/gitlab.default.example origin/8-17-stable:lib/support/init.d/gitlab.default.example +``` + Ensure you're still up-to-date with the latest init script changes: ```bash @@ -254,3 +264,5 @@ sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. [yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-17-stable/config/gitlab.yml.example +[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-17-stable/lib/support/init.d/gitlab.default.example +[pages-admin]: ../administration/pages/source.md diff --git a/doc/update/8.17-to-9.0.md b/doc/update/8.17-to-9.0.md index 1fe38cf8d2a..626507c0482 100644 --- a/doc/update/8.17-to-9.0.md +++ b/doc/update/8.17-to-9.0.md @@ -149,7 +149,7 @@ sudo -u git -H git checkout v5.0.0 #### New configuration options for `gitlab.yml` -There are new configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`: +There might be configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`: ```sh cd /home/git/gitlab @@ -189,7 +189,7 @@ Update your current configuration as follows, replacing with your storages names **For Omnibus installations** -1. Upate your `/etc/gitlab/gitlab.rb`, from +1. Update your `/etc/gitlab/gitlab.rb`, from ```ruby git_data_dirs({ @@ -260,6 +260,14 @@ See [smtp_settings.rb.sample] as an example. #### Init script +There might be new configuration options available for [`gitlab.default.example`][gl-example]. View them with the command below and apply them manually to your current `/etc/default/gitlab`: + +```sh +cd /home/git/gitlab + +git diff origin/8-17-stable:lib/support/init.d/gitlab.default.example origin/9-0-stable:lib/support/init.d/gitlab.default.example +``` + Ensure you're still up-to-date with the latest init script changes: ```bash @@ -319,3 +327,4 @@ sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. [yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-0-stable/config/gitlab.yml.example +[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-0-stable/lib/support/init.d/gitlab.default.example -- cgit v1.2.1 From 435458d2b14eadd1768b7b0a14f5966633f02f83 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Mon, 6 Mar 2017 00:03:21 -0500 Subject: Update API on frontend to use v4 Use options object to pass params for project endpoint --- app/assets/javascripts/api.js | 10 +++++----- app/assets/javascripts/project_select.js | 4 ++-- app/assets/javascripts/search.js | 2 +- app/views/help/ui.html.haml | 2 +- lib/gitlab/gon_helper.rb | 2 +- spec/javascripts/project_title_spec.js | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index a0946eb392a..e5f36c84987 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -51,15 +51,15 @@ var Api = { }); }, // Return projects list. Filtered by query - projects: function(query, order, callback) { + projects: function(query, options, callback) { var url = Api.buildUrl(Api.projectsPath); return $.ajax({ url: url, - data: { + data: $.extend({ search: query, - order_by: order, - per_page: 20 - }, + per_page: 20, + membership: true + }, options), dataType: "json" }).done(function(projects) { return callback(projects); diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index f80e765ce30..3c1c1e7dceb 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -35,7 +35,7 @@ if (this.groupId) { return Api.groupProjects(this.groupId, term, projectsCallback); } else { - return Api.projects(term, orderBy, projectsCallback); + return Api.projects(term, { order_by: orderBy }, projectsCallback); } }, url: function(project) { @@ -84,7 +84,7 @@ if (_this.groupId) { return Api.groupProjects(_this.groupId, query.term, projectsCallback); } else { - return Api.projects(query.term, _this.orderBy, projectsCallback); + return Api.projects(query.term, { order_by: _this.orderBy }, projectsCallback); } }; })(this), diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js index e66418beeab..15f5963353a 100644 --- a/app/assets/javascripts/search.js +++ b/app/assets/javascripts/search.js @@ -47,7 +47,7 @@ fields: ['name'] }, data: function(term, callback) { - return Api.projects(term, 'id', function(data) { + return Api.projects(term, { order_by: 'id' }, function(data) { data.unshift({ name_with_namespace: 'Any' }); diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index 87f9b503989..1fb2c6271ad 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -410,7 +410,7 @@ :javascript $('#js-project-dropdown').glDropdown({ data: function (term, callback) { - Api.projects(term, "last_activity_at", function (data) { + Api.projects(term, { order_by: 'last_activity_at' }, function (data) { callback(data); }); }, diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index 6c275a8d5de..5ab84266b7d 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -1,7 +1,7 @@ module Gitlab module GonHelper def add_gon_variables - gon.api_version = 'v3' # v4 Is not officially released yet, therefore can't be considered as "frozen" + gon.api_version = 'v4' gon.default_avatar_url = URI.join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s gon.max_file_size = current_application_settings.max_attachment_size gon.asset_host = ActionController::Base.asset_host diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js index 69d9587771f..3a1d4e2440f 100644 --- a/spec/javascripts/project_title_spec.js +++ b/spec/javascripts/project_title_spec.js @@ -26,7 +26,7 @@ require('~/project'); var fakeAjaxResponse = function fakeAjaxResponse(req) { var d; expect(req.url).toBe('/api/v3/projects.json?simple=true'); - expect(req.data).toEqual({ search: '', order_by: 'last_activity_at', per_page: 20 }); + expect(req.data).toEqual({ search: '', order_by: 'last_activity_at', per_page: 20, membership: true }); d = $.Deferred(); d.resolve(this.projects_data); return d.promise(); -- cgit v1.2.1 From 6bffc17f7d04f6ecd99d896ed299060724032a3b Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 14 Mar 2017 12:24:53 +0100 Subject: Add changelog entry for project status caching fix --- changelogs/unreleased/fix-gb-dashboard-commit-status-caching.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/fix-gb-dashboard-commit-status-caching.yml diff --git a/changelogs/unreleased/fix-gb-dashboard-commit-status-caching.yml b/changelogs/unreleased/fix-gb-dashboard-commit-status-caching.yml new file mode 100644 index 00000000000..4db684c40b2 --- /dev/null +++ b/changelogs/unreleased/fix-gb-dashboard-commit-status-caching.yml @@ -0,0 +1,4 @@ +--- +title: Resolve project pipeline status caching problem on dashboard +merge_request: 9895 +author: -- cgit v1.2.1 From c7cecae616702a46430ed41e283912ddc22f2612 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 14 Mar 2017 11:32:58 +0000 Subject: added eventhub to emit update tokens event --- app/assets/javascripts/boards/boards_bundle.js | 5 +++-- app/assets/javascripts/boards/components/issue_card_inner.js | 4 +++- app/assets/javascripts/boards/eventhub.js | 3 +++ 3 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 app/assets/javascripts/boards/eventhub.js diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js index 4d60fedaeb8..3874c2819a5 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -3,6 +3,7 @@ /* global BoardService */ import FilteredSearchBoards from './filtered_search_boards'; +import eventHub from './eventhub'; window.Vue = require('vue'); window.Vue.use(require('vue-resource')); @@ -65,10 +66,10 @@ $(() => { this.filterManager = new FilteredSearchBoards(Store.filter, true); // Listen for updateTokens event - this.$on('updateTokens', this.updateTokens); + eventHub.$on('updateTokens', this.updateTokens); }, beforeDestroy() { - this.$off('updateTokens', this.updateTokens); + eventHub.$off('updateTokens', this.updateTokens); }, mounted () { Store.disabled = this.disabled; diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js index 3d57ec429c6..69e30cec4c5 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.js +++ b/app/assets/javascripts/boards/components/issue_card_inner.js @@ -1,4 +1,6 @@ /* global Vue */ +import eventHub from '../eventhub'; + (() => { const Store = gl.issueBoards.BoardsStore; @@ -54,7 +56,7 @@ Store.updateFiltersUrl(); - gl.IssueBoardsApp.$emit('updateTokens'); + eventHub.$emit('updateTokens'); }, labelStyle(label) { return { diff --git a/app/assets/javascripts/boards/eventhub.js b/app/assets/javascripts/boards/eventhub.js new file mode 100644 index 00000000000..0948c2e5352 --- /dev/null +++ b/app/assets/javascripts/boards/eventhub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); -- cgit v1.2.1 From c9fbbb3ae2c7f0eb44b0f973155d68e678149544 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 14 Mar 2017 19:56:37 +0800 Subject: Disable rubocop for down method --- db/migrate/20161201160452_migrate_project_statistics.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/db/migrate/20161201160452_migrate_project_statistics.rb b/db/migrate/20161201160452_migrate_project_statistics.rb index 8386f7f9d4f..82fbdf02444 100644 --- a/db/migrate/20161201160452_migrate_project_statistics.rb +++ b/db/migrate/20161201160452_migrate_project_statistics.rb @@ -16,6 +16,7 @@ class MigrateProjectStatistics < ActiveRecord::Migration remove_column :projects, :commit_count end + # rubocop: disable Migration/AddColumn def down add_column :projects, :repository_size, :float, default: 0.0 add_column :projects, :commit_count, :integer, default: 0 -- cgit v1.2.1 From 1324a6b3050a121eae722258302ce46bf2636ac1 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 14 Mar 2017 13:03:17 +0100 Subject: Use google-protobuf 3.2.0.1 This sub-patch release extends support in pre-compiled libraries down to glibc 2.12. https://github.com/google/protobuf/issues/2783 Closes https://gitlab.com/gitlab-org/gitlab-ce/issues/29084 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index c60c045a4c2..e38f45b8e98 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -304,7 +304,7 @@ GEM multi_json (~> 1.10) retriable (~> 1.4) signet (~> 0.6) - google-protobuf (3.2.0) + google-protobuf (3.2.0.1) googleauth (0.5.1) faraday (~> 0.9) jwt (~> 1.4) -- cgit v1.2.1 From f67d8eb1da269150764224cea1807195cdf2ffb5 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 14 Mar 2017 20:03:22 +0800 Subject: Drop the index only for postgresql, because mysql cannot simply drop the index without dropping the corresponding foreign key, and we certainly don't want to drop the foreign key here. --- db/migrate/20170130204620_add_index_to_project_authorizations.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/db/migrate/20170130204620_add_index_to_project_authorizations.rb b/db/migrate/20170130204620_add_index_to_project_authorizations.rb index a8c504f265a..629b49436e3 100644 --- a/db/migrate/20170130204620_add_index_to_project_authorizations.rb +++ b/db/migrate/20170130204620_add_index_to_project_authorizations.rb @@ -10,6 +10,7 @@ class AddIndexToProjectAuthorizations < ActiveRecord::Migration end def down - remove_index(:project_authorizations, :project_id) + remove_index(:project_authorizations, :project_id) if + Gitlab::Database.postgresql? end end -- cgit v1.2.1 From 96fe1856da46517b028fe0ddac89f314f25c8855 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Thu, 9 Mar 2017 16:51:20 +0200 Subject: Fix relative position calculation --- app/models/concerns/relative_positioning.rb | 20 ++++++++++------- spec/models/concerns/relative_positioning_spec.rb | 27 +++++++++++++++++++++++ 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb index 603f2dd7e5d..ec23c8a08fb 100644 --- a/app/models/concerns/relative_positioning.rb +++ b/app/models/concerns/relative_positioning.rb @@ -3,6 +3,7 @@ module RelativePositioning MIN_POSITION = 0 MAX_POSITION = Gitlab::Database::MAX_INT_VALUE + DISTANCE = 500 included do after_save :save_positionable_neighbours @@ -49,7 +50,9 @@ module RelativePositioning pos_before = before.relative_position pos_after = after.relative_position - if pos_after && (pos_before == pos_after) + # We can't insert an issue between two other if the distance is 1 or 0 + # so we need to handle this collision properly + if pos_after && (pos_after - pos_before).abs <= 1 self.relative_position = pos_before before.move_before(self) after.move_after(self) @@ -75,19 +78,20 @@ module RelativePositioning private # This method takes two integer values (positions) and - # calculates some random position between them. The range is huge as - # the maximum integer value is 2147483647. Ideally, the calculated value would be - # exactly between those terminating values, but this will introduce possibility of a race condition - # so two or more issues can get the same value, we want to avoid that and we also want to avoid - # using a lock here. If we have two issues with distance more than one thousand, we are OK. - # Given the huge range of possible values that integer can fit we shoud never face a problem. + # calculates the position between them. The range is huge as + # the maximum integer value is 2147483647. We are incrementing position by 1000 every time + # when we have enough space. If distance is less then 500 we are calculating an average number def position_between(pos_before, pos_after) pos_before ||= MIN_POSITION pos_after ||= MAX_POSITION pos_before, pos_after = [pos_before, pos_after].sort - rand(pos_before.next..pos_after.pred) + if pos_after - pos_before > DISTANCE * 2 + pos_before + DISTANCE + else + pos_before + (pos_after - pos_before) / 2 + end end def save_positionable_neighbours diff --git a/spec/models/concerns/relative_positioning_spec.rb b/spec/models/concerns/relative_positioning_spec.rb index 69906382545..12f44bfe0a5 100644 --- a/spec/models/concerns/relative_positioning_spec.rb +++ b/spec/models/concerns/relative_positioning_spec.rb @@ -100,5 +100,32 @@ describe Issue, 'RelativePositioning' do expect(new_issue.relative_position).to be > issue.relative_position expect(issue.relative_position).to be < issue1.relative_position end + + it 'positions issues between other two if distance is 1' do + issue1.update relative_position: issue.relative_position + 1 + + new_issue.move_between(issue, issue1) + + expect(new_issue.relative_position).to be > issue.relative_position + expect(issue.relative_position).to be < issue1.relative_position + end + + it 'positions issue closer to before-issue if distance is big enough' do + issue.update relative_position: 100 + issue1.update relative_position: 6000 + + new_issue.move_between(issue, issue1) + + expect(new_issue.relative_position).to eq(100 + RelativePositioning::DISTANCE) + end + + it 'positions issue in the middle of other two if distance is not big enough' do + issue.update relative_position: 100 + issue1.update relative_position: 400 + + new_issue.move_between(issue, issue1) + + expect(new_issue.relative_position).to eq(250) + end end end -- cgit v1.2.1 From 67686e38fdb1d9c427a39ff1862af691ccd4e598 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Thu, 9 Mar 2017 19:23:36 +0200 Subject: Added migration to reset existing relative_position for issues --- ...20170309171644_reset_relative_position_for_issue.rb | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 db/post_migrate/20170309171644_reset_relative_position_for_issue.rb diff --git a/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb b/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb new file mode 100644 index 00000000000..ce4be131d40 --- /dev/null +++ b/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb @@ -0,0 +1,18 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class ResetRelativePositionForIssue < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + execute <<-EOS + UPDATE issues SET relative_position = NULL + WHERE issues.relative_position IS NOT NULL; + EOS + end + + def down + end +end -- cgit v1.2.1 From 5670777735a615b511c3282e8fc79b67c74669bc Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Fri, 10 Mar 2017 17:12:31 +0200 Subject: [Issue sorting] Filling positions preferable in the middle --- app/models/concerns/relative_positioning.rb | 23 +++++++------ spec/models/concerns/relative_positioning_spec.rb | 40 +++++++++++++++-------- 2 files changed, 39 insertions(+), 24 deletions(-) diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb index ec23c8a08fb..f8ab16a9f4c 100644 --- a/app/models/concerns/relative_positioning.rb +++ b/app/models/concerns/relative_positioning.rb @@ -2,6 +2,7 @@ module RelativePositioning extend ActiveSupport::Concern MIN_POSITION = 0 + START_POSITION = Gitlab::Database::MAX_INT_VALUE / 2 MAX_POSITION = Gitlab::Database::MAX_INT_VALUE DISTANCE = 500 @@ -9,10 +10,6 @@ module RelativePositioning after_save :save_positionable_neighbours end - def min_relative_position - self.class.in_projects(project.id).minimum(:relative_position) - end - def max_relative_position self.class.in_projects(project.id).maximum(:relative_position) end @@ -27,7 +24,7 @@ module RelativePositioning maximum(:relative_position) end - prev_pos || MIN_POSITION + prev_pos end def next_relative_position @@ -40,7 +37,7 @@ module RelativePositioning minimum(:relative_position) end - next_pos || MAX_POSITION + next_pos end def move_between(before, after) @@ -72,7 +69,7 @@ module RelativePositioning end def move_to_end - self.relative_position = position_between(max_relative_position, MAX_POSITION) + self.relative_position = position_between(max_relative_position || START_POSITION, MAX_POSITION) end private @@ -87,10 +84,16 @@ module RelativePositioning pos_before, pos_after = [pos_before, pos_after].sort - if pos_after - pos_before > DISTANCE * 2 - pos_before + DISTANCE + if pos_after - pos_before < DISTANCE * 2 + (pos_after + pos_before) / 2 else - pos_before + (pos_after - pos_before) / 2 + if pos_before == MIN_POSITION + pos_after - DISTANCE + elsif pos_after == MAX_POSITION + pos_before + DISTANCE + else + (pos_after + pos_before) / 2 + end end end diff --git a/spec/models/concerns/relative_positioning_spec.rb b/spec/models/concerns/relative_positioning_spec.rb index 12f44bfe0a5..fbae9efcd98 100644 --- a/spec/models/concerns/relative_positioning_spec.rb +++ b/spec/models/concerns/relative_positioning_spec.rb @@ -12,12 +12,6 @@ describe Issue, 'RelativePositioning' do end end - describe '#min_relative_position' do - it 'returns maximum position' do - expect(issue.min_relative_position).to eq issue.relative_position - end - end - describe '#max_relative_position' do it 'returns maximum position' do expect(issue.max_relative_position).to eq issue1.relative_position @@ -29,8 +23,8 @@ describe Issue, 'RelativePositioning' do expect(issue1.prev_relative_position).to eq issue.relative_position end - it 'returns minimum position if there is no issue above' do - expect(issue.prev_relative_position).to eq RelativePositioning::MIN_POSITION + it 'returns nil if there is no issue above' do + expect(issue.prev_relative_position).to eq nil end end @@ -39,8 +33,8 @@ describe Issue, 'RelativePositioning' do expect(issue.next_relative_position).to eq issue1.relative_position end - it 'returns next position if there is no issue below' do - expect(issue1.next_relative_position).to eq RelativePositioning::MAX_POSITION + it 'returns nil if there is no issue below' do + expect(issue1.next_relative_position).to eq nil end end @@ -110,15 +104,33 @@ describe Issue, 'RelativePositioning' do expect(issue.relative_position).to be < issue1.relative_position end - it 'positions issue closer to before-issue if distance is big enough' do - issue.update relative_position: 100 - issue1.update relative_position: 6000 + it 'positions issue in the middle of other two if distance is big enough' do + issue.update relative_position: 6000 + issue1.update relative_position: 10000 new_issue.move_between(issue, issue1) - expect(new_issue.relative_position).to eq(100 + RelativePositioning::DISTANCE) + expect(new_issue.relative_position).to eq(8000) + end + + it 'positions issue closer to the middle if we are at the very top' do + issue1.update relative_position: 6000 + + new_issue.move_between(nil, issue1) + + expect(new_issue.relative_position).to eq(6000 - RelativePositioning::DISTANCE) + end + + it 'positions issue closer to the middle if we are at the very bottom' do + issue.update relative_position: 6000 + issue1.update relative_position: nil + + new_issue.move_between(issue, nil) + + expect(new_issue.relative_position).to eq(6000 + RelativePositioning::DISTANCE) end + it 'positions issue in the middle of other two if distance is not big enough' do issue.update relative_position: 100 issue1.update relative_position: 400 -- cgit v1.2.1 From b84723ac8bf8572c3d261980ab053dda52bc78dd Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Fri, 10 Mar 2017 17:17:55 +0200 Subject: [Issue Sorting] Improve migration --- .../20170309171644_reset_relative_position_for_issue.rb | 7 +++---- spec/models/concerns/relative_positioning_spec.rb | 1 - 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb b/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb index ce4be131d40..b61dd7cfc61 100644 --- a/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb +++ b/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb @@ -7,10 +7,9 @@ class ResetRelativePositionForIssue < ActiveRecord::Migration DOWNTIME = false def up - execute <<-EOS - UPDATE issues SET relative_position = NULL - WHERE issues.relative_position IS NOT NULL; - EOS + update_column_in_batches(:issues, :relative_position, nil) do |table, query| + query.where(table[:relative_position].not_eq(nil)) + end end def down diff --git a/spec/models/concerns/relative_positioning_spec.rb b/spec/models/concerns/relative_positioning_spec.rb index fbae9efcd98..ebbf14fb5ba 100644 --- a/spec/models/concerns/relative_positioning_spec.rb +++ b/spec/models/concerns/relative_positioning_spec.rb @@ -130,7 +130,6 @@ describe Issue, 'RelativePositioning' do expect(new_issue.relative_position).to eq(6000 + RelativePositioning::DISTANCE) end - it 'positions issue in the middle of other two if distance is not big enough' do issue.update relative_position: 100 issue1.update relative_position: 400 -- cgit v1.2.1 From e752d6d157321f9f10d70cdef1ce1992e263634f Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Fri, 10 Mar 2017 19:04:37 +0200 Subject: [Issue sorting]Addressed review comments --- .flayignore | 1 + app/models/concerns/relative_positioning.rb | 77 ++++++++++++++++------- spec/models/concerns/relative_positioning_spec.rb | 66 ++++++++++++++++++- 3 files changed, 119 insertions(+), 25 deletions(-) diff --git a/.flayignore b/.flayignore index fc64b0b5892..47597025115 100644 --- a/.flayignore +++ b/.flayignore @@ -2,3 +2,4 @@ lib/gitlab/sanitizers/svg/whitelist.rb lib/gitlab/diff/position_tracer.rb app/policies/project_policy.rb +app/models/concerns/relative_positioning.rb diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb index f8ab16a9f4c..f1d8532a6d6 100644 --- a/app/models/concerns/relative_positioning.rb +++ b/app/models/concerns/relative_positioning.rb @@ -4,7 +4,7 @@ module RelativePositioning MIN_POSITION = 0 START_POSITION = Gitlab::Database::MAX_INT_VALUE / 2 MAX_POSITION = Gitlab::Database::MAX_INT_VALUE - DISTANCE = 500 + IDEAL_DISTANCE = 500 included do after_save :save_positionable_neighbours @@ -44,55 +44,86 @@ module RelativePositioning return move_after(before) unless after return move_before(after) unless before + # If there is no place to insert an issue we need to create one by moving the before issue closer + # to its predecessor. This process will recursively move all the predecessors until we have a place + if (after.relative_position - before.relative_position) < 2 + before.move_before + @positionable_neighbours = [before] + end + + self.relative_position = position_between(before.relative_position, after.relative_position) + end + + def move_after(before = self) pos_before = before.relative_position - pos_after = after.relative_position + pos_after = before.next_relative_position - # We can't insert an issue between two other if the distance is 1 or 0 - # so we need to handle this collision properly - if pos_after && (pos_after - pos_before).abs <= 1 - self.relative_position = pos_before - before.move_before(self) - after.move_after(self) + if before.shift_after? + issue_to_move = self.class.in_projects(project.id).find_by!(relative_position: pos_after) + issue_to_move.move_after + @positionable_neighbours = [issue_to_move] - @positionable_neighbours = [before, after] - else - self.relative_position = position_between(pos_before, pos_after) + pos_after = issue_to_move.relative_position end - end - def move_before(after) - self.relative_position = position_between(after.prev_relative_position, after.relative_position) + self.relative_position = position_between(pos_before, pos_after) end - def move_after(before) - self.relative_position = position_between(before.relative_position, before.next_relative_position) + def move_before(after = self) + pos_after = after.relative_position + pos_before = after.prev_relative_position + + if after.shift_before? + issue_to_move = self.class.in_projects(project.id).find_by!(relative_position: pos_before) + issue_to_move.move_before + @positionable_neighbours = [issue_to_move] + + pos_before = issue_to_move.relative_position + end + + self.relative_position = position_between(pos_before, pos_after) end def move_to_end self.relative_position = position_between(max_relative_position || START_POSITION, MAX_POSITION) end + # Indicates if there is an issue that should be shifted to free the place + def shift_after? + next_pos = next_relative_position + next_pos && (next_pos - relative_position) == 1 + end + + # Indicates if there is an issue that should be shifted to free the place + def shift_before? + prev_pos = prev_relative_position + prev_pos && (relative_position - prev_pos) == 1 + end + private # This method takes two integer values (positions) and # calculates the position between them. The range is huge as - # the maximum integer value is 2147483647. We are incrementing position by 1000 every time - # when we have enough space. If distance is less then 500 we are calculating an average number + # the maximum integer value is 2147483647. We are incrementing position by IDEAL_DISTANCE * 2 every time + # when we have enough space. If distance is less then IDEAL_DISTANCE we are calculating an average number def position_between(pos_before, pos_after) pos_before ||= MIN_POSITION pos_after ||= MAX_POSITION pos_before, pos_after = [pos_before, pos_after].sort - if pos_after - pos_before < DISTANCE * 2 - (pos_after + pos_before) / 2 + halfway = (pos_after + pos_before) / 2 + distance_to_halfway = pos_after - halfway + + if distance_to_halfway < IDEAL_DISTANCE + halfway else if pos_before == MIN_POSITION - pos_after - DISTANCE + pos_after - IDEAL_DISTANCE elsif pos_after == MAX_POSITION - pos_before + DISTANCE + pos_before + IDEAL_DISTANCE else - (pos_after + pos_before) / 2 + halfway end end end diff --git a/spec/models/concerns/relative_positioning_spec.rb b/spec/models/concerns/relative_positioning_spec.rb index ebbf14fb5ba..255b584a85e 100644 --- a/spec/models/concerns/relative_positioning_spec.rb +++ b/spec/models/concerns/relative_positioning_spec.rb @@ -66,6 +66,34 @@ describe Issue, 'RelativePositioning' do end end + describe '#shift_after?' do + it 'returns true' do + issue.update(relative_position: issue1.relative_position - 1) + + expect(issue.shift_after?).to be_truthy + end + + it 'returns false' do + issue.update(relative_position: issue1.relative_position - 2) + + expect(issue.shift_after?).to be_falsey + end + end + + describe '#shift_before?' do + it 'returns true' do + issue.update(relative_position: issue1.relative_position + 1) + + expect(issue.shift_before?).to be_truthy + end + + it 'returns false' do + issue.update(relative_position: issue1.relative_position + 2) + + expect(issue.shift_before?).to be_falsey + end + end + describe '#move_between' do it 'positions issue between two other' do new_issue.move_between(issue, issue1) @@ -118,7 +146,7 @@ describe Issue, 'RelativePositioning' do new_issue.move_between(nil, issue1) - expect(new_issue.relative_position).to eq(6000 - RelativePositioning::DISTANCE) + expect(new_issue.relative_position).to eq(6000 - RelativePositioning::IDEAL_DISTANCE) end it 'positions issue closer to the middle if we are at the very bottom' do @@ -127,7 +155,7 @@ describe Issue, 'RelativePositioning' do new_issue.move_between(issue, nil) - expect(new_issue.relative_position).to eq(6000 + RelativePositioning::DISTANCE) + expect(new_issue.relative_position).to eq(6000 + RelativePositioning::IDEAL_DISTANCE) end it 'positions issue in the middle of other two if distance is not big enough' do @@ -138,5 +166,39 @@ describe Issue, 'RelativePositioning' do expect(new_issue.relative_position).to eq(250) end + + it 'positions issue in the middle of other two is there is no place' do + issue.update relative_position: 100 + issue1.update relative_position: 101 + + new_issue.move_between(issue, issue1) + + expect(new_issue.relative_position).to be_between(issue.relative_position, issue1.relative_position) + end + + it 'uses rebalancing if there is no place' do + issue.update relative_position: 100 + issue1.update relative_position: 101 + issue2 = create(:issue, relative_position: 102, project: project) + new_issue.update relative_position: 103 + + new_issue.move_between(issue1, issue2) + new_issue.save! + + expect(new_issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position) + expect(issue.reload.relative_position).not_to eq(100) + end + + it 'positions issue right if we pass none-sequential parameters' do + issue.update relative_position: 99 + issue1.update relative_position: 101 + issue2 = create(:issue, relative_position: 102, project: project) + new_issue.update relative_position: 103 + + new_issue.move_between(issue, issue2) + new_issue.save! + + expect(new_issue.relative_position).to be(100) + end end end -- cgit v1.2.1 From af8cc2e064bb97a8a1801521735d5403b189bfb5 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 14 Mar 2017 20:13:36 +0800 Subject: Use `remove_foreign_key :timelogs, name: '...'` Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9908#note_25324225 --- db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb b/db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb index 676e18cddd3..a7d4e141a1a 100644 --- a/db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb +++ b/db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb @@ -49,15 +49,8 @@ class AddForeignKeysToTimelogs < ActiveRecord::Migration Timelog.where('issue_id IS NOT NULL').update_all("trackable_id = issue_id, trackable_type = 'Issue'") Timelog.where('merge_request_id IS NOT NULL').update_all("trackable_id = merge_request_id, trackable_type = 'MergeRequest'") - constraint = - if Gitlab::Database.postgresql? - 'CONSTRAINT' - else - 'FOREIGN KEY' - end - - execute "ALTER TABLE timelogs DROP #{constraint} fk_timelogs_issues_issue_id" - execute "ALTER TABLE timelogs DROP #{constraint} fk_timelogs_merge_requests_merge_request_id" + remove_foreign_key :timelogs, name: 'fk_timelogs_issues_issue_id' + remove_foreign_key :timelogs, name: 'fk_timelogs_merge_requests_merge_request_id' remove_columns :timelogs, :issue_id, :merge_request_id end -- cgit v1.2.1 From 82f61410b65c6044aa630d5f5b27fada207b870e Mon Sep 17 00:00:00 2001 From: Chris Peressini Date: Tue, 14 Mar 2017 12:23:44 +0000 Subject: Change Canccel button class Used to be `btn-default`, change it to `btn-cancel` --- app/views/admin/applications/_form.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml index c689b26d6e6..061f8991b11 100644 --- a/app/views/admin/applications/_form.html.haml +++ b/app/views/admin/applications/_form.html.haml @@ -26,4 +26,4 @@ .form-actions = f.submit 'Submit', class: "btn btn-save wide" - = link_to "Cancel", admin_applications_path, class: "btn btn-default" + = link_to "Cancel", admin_applications_path, class: "btn btn-cancel" -- cgit v1.2.1 From 83e36064998f77f40c534bad531b6cea19ec198b Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 14 Mar 2017 21:40:58 +0800 Subject: Split to two commands, feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9908#note_25331127 --- .gitlab-ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b1ca61604d5..406a0f3dcad 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -227,7 +227,8 @@ rake db:rollback: <<: *use-db <<: *dedicated-runner script: - - bundle exec rake db:rollback db:migrate STEP=120 + - bundle exec rake db:rollback STEP=120 + - bundle exec rake db:migrate rake db:seed_fu: stage: test -- cgit v1.2.1 From 84561349ffa7aa079f5bd371ba51bef02ee8f6df Mon Sep 17 00:00:00 2001 From: Adam Niedzielski Date: Tue, 14 Mar 2017 14:45:38 +0100 Subject: Describe polling with ETag caching --- CONTRIBUTING.md | 5 +++-- doc/development/polling.md | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 doc/development/polling.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9a6e3feec4c..ae143c58290 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -403,8 +403,8 @@ There are a few rules to get your merge request accepted: - Avoid repeated polling of endpoints that require a significant amount of overhead - Check for N+1 queries via the SQL log or [`QueryRecorder`](https://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html) - Avoid repeated access of filesystem -1. If you need polling to support real-time features, consider using this [described long - polling approach](https://gitlab.com/gitlab-org/gitlab-ce/issues/26926). +1. If you need polling to support real-time features, please use + [polling with ETag caching][polling-etag]. 1. Changes after submitting the merge request should be in separate commits (no squashing). If necessary, you will be asked to squash when the review is over, before merging. @@ -547,6 +547,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor [UX Guide for GitLab]: http://docs.gitlab.com/ce/development/ux_guide/ [license-finder-doc]: doc/development/licensing.md [GitLab Inc engineering workflow]: https://about.gitlab.com/handbook/engineering/workflow/#labelling-issues +[polling-etag]: https://docs.gitlab.com/ce/development/polling.html [^1]: Specs other than JavaScript specs are considered backend code. Haml changes are considered backend code if they include Ruby code other than just diff --git a/doc/development/polling.md b/doc/development/polling.md new file mode 100644 index 00000000000..a086aca6697 --- /dev/null +++ b/doc/development/polling.md @@ -0,0 +1,41 @@ +# Polling with ETag caching + +Polling for changes (repeatedly asking server if there are any new changes) +introduces high load on a GitLab instance, because it usually requires +executing at least a few SQL queries. This makes scaling large GitLab +instances (like GitLab.com) very difficult so we do not allow adding new +features that require polling and hit the database. + +Instead you should use polling mechanism with ETag caching in Redis. + +## How to use it + +1. Add the path of the endpoint which you want to poll to + `Gitlab::EtagCaching::Middleware`. +1. Implement cache invalidation for the path of your endpoint using + `Gitlab::EtagCaching::Store`. Whenever a resource changes you + have to invalidate the ETag for the path that depends on this + resource. +1. Check that the mechanism works: + - requests should return status code 304 + - there should be no SQL queries logged in `log/development.log` + +## How it works + +1. Whenever a resource changes we generate a random value and store it in + Redis. +1. When a client makes a request we set the `ETag` response header to the value + from Redis. +1. The client caches the response (client-side caching) and sends the ETag as + the `If-None-Modified` header with every subsequent request for the same + resource. +1. If the `If-None-Modified` header matches the current value in Redis we know + that the resource did not change so we can send 304 response immediately, + without querying the database at all. The client's browser will use the + cached response. +1. If the `If-None-Modified` header does not match the current value in Redis + we have to generate a new response, because the resource changed. + +For more information see: +- [RFC 7232](https://tools.ietf.org/html/rfc7232) +- [ETag proposal](https://gitlab.com/gitlab-org/gitlab-ce/issues/26926) -- cgit v1.2.1 From 1cba12d35f0e2bad8b2903a58c883437a701fee4 Mon Sep 17 00:00:00 2001 From: szymon Date: Tue, 14 Mar 2017 14:03:28 +0000 Subject: Update using_docker_images.md --- doc/ci/docker/using_docker_images.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md index 00787323b6b..f025a7e3496 100644 --- a/doc/ci/docker/using_docker_images.md +++ b/doc/ci/docker/using_docker_images.md @@ -170,13 +170,17 @@ services: ``` When the job is run, `tutum/wordpress` will be started and you will have -access to it from your build container under the hostname `tutum__wordpress`. +access to it from your build container under the hostnames `tutum-wordpress` +(requires GitLab Runner v1.1.0 or newer) and `tutum__wordpress`. -The alias hostname for the service is made from the image name following these +*Note: hostname with underscores is not RFC valid and may cause problems in 3rd party applications.* + +The alias hostnames for the service are made from the image name following these rules: 1. Everything after `:` is stripped -2. Slash (`/`) is replaced with double underscores (`__`) +2. Slash (`/`) is replaced with double underscores (`__`) - primary alias +3. Slash (`/`) is replaced with dash (`-`) - secondary alias, requires GitLab Runner v1.1.0 or newer ## Configuring services -- cgit v1.2.1 From 283789c50913ec95725e83b4d270151347d205cc Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Tue, 14 Mar 2017 12:56:07 +0000 Subject: Fix intermittent spec failures in notify_spec.rb --- spec/mailers/notify_spec.rb | 231 +++++++++++++++++++++++--------------------- 1 file changed, 120 insertions(+), 111 deletions(-) diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index b692142713f..e822d7eb348 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -8,6 +8,15 @@ describe Notify do include_context 'gitlab email notification' + def have_referable_subject(referable, reply: false) + prefix = referable.project.name if referable.project + prefix = "Re: #{prefix}" if reply + + suffix = "#{referable.title} (#{referable.to_reference})" + + have_subject [prefix, suffix].compact.join(' | ') + end + context 'for a project' do describe 'items that are assignable, the email' do let(:current_user) { create(:user, email: "current@email.com") } @@ -41,11 +50,11 @@ describe Notify do it_behaves_like 'an unsubscribeable thread' it 'has the correct subject' do - is_expected.to have_subject /#{project.name} \| #{issue.title} \(##{issue.iid}\)/ + is_expected.to have_referable_subject(issue) end it 'contains a link to the new issue' do - is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/ + is_expected.to have_body_text namespace_project_issue_path(project.namespace, project, issue) end context 'when enabled email_author_in_body' do @@ -55,7 +64,7 @@ describe Notify do it 'contains a link to note author' do is_expected.to have_body_text issue.author_name - is_expected.to have_body_text /wrote\:/ + is_expected.to have_body_text 'wrote:' end end end @@ -66,7 +75,7 @@ describe Notify do it_behaves_like 'it should show Gmail Actions View Issue link' it 'contains the description' do - is_expected.to have_body_text /#{issue_with_description.description}/ + is_expected.to have_body_text issue_with_description.description end end @@ -87,19 +96,19 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/ + is_expected.to have_referable_subject(issue, reply: true) end it 'contains the name of the previous assignee' do - is_expected.to have_body_text /#{previous_assignee.name}/ + is_expected.to have_body_text previous_assignee.name end it 'contains the name of the new assignee' do - is_expected.to have_body_text /#{assignee.name}/ + is_expected.to have_body_text assignee.name end it 'contains a link to the issue' do - is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/ + is_expected.to have_body_text namespace_project_issue_path(project.namespace, project, issue) end end @@ -121,15 +130,15 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/ + is_expected.to have_referable_subject(issue, reply: true) end it 'contains the names of the added labels' do - is_expected.to have_body_text /foo, bar, and baz/ + is_expected.to have_body_text 'foo, bar, and baz' end it 'contains a link to the issue' do - is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/ + is_expected.to have_body_text namespace_project_issue_path(project.namespace, project, issue) end end @@ -150,19 +159,19 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/i + is_expected.to have_referable_subject(issue, reply: true) end it 'contains the new status' do - is_expected.to have_body_text /#{status}/i + is_expected.to have_body_text status end it 'contains the user name' do - is_expected.to have_body_text /#{current_user.name}/i + is_expected.to have_body_text current_user.name end it 'contains a link to the issue' do - is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/ + is_expected.to have_body_text(namespace_project_issue_path project.namespace, project, issue) end end @@ -181,7 +190,7 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/i + is_expected.to have_referable_subject(issue, reply: true) end it 'contains link to new issue' do @@ -191,7 +200,7 @@ describe Notify do end it 'contains a link to the original issue' do - is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/ + is_expected.to have_body_text namespace_project_issue_path(project.namespace, project, issue) end end end @@ -212,19 +221,19 @@ describe Notify do it_behaves_like 'an unsubscribeable thread' it 'has the correct subject' do - is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/ + is_expected.to have_referable_subject(merge_request) end it 'contains a link to the new merge request' do - is_expected.to have_body_text /#{namespace_project_merge_request_path(project.namespace, project, merge_request)}/ + is_expected.to have_body_text namespace_project_merge_request_path(project.namespace, project, merge_request) end it 'contains the source branch for the merge request' do - is_expected.to have_body_text /#{merge_request.source_branch}/ + is_expected.to have_body_text merge_request.source_branch end it 'contains the target branch for the merge request' do - is_expected.to have_body_text /#{merge_request.target_branch}/ + is_expected.to have_body_text merge_request.target_branch end context 'when enabled email_author_in_body' do @@ -234,7 +243,7 @@ describe Notify do it 'contains a link to note author' do is_expected.to have_body_text merge_request.author_name - is_expected.to have_body_text /wrote\:/ + is_expected.to have_body_text 'wrote:' end end end @@ -246,7 +255,7 @@ describe Notify do it_behaves_like "an unsubscribeable thread" it 'contains the description' do - is_expected.to have_body_text /#{merge_request_with_description.description}/ + is_expected.to have_body_text merge_request_with_description.description end end @@ -267,19 +276,19 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/ + is_expected.to have_referable_subject(merge_request, reply: true) end it 'contains the name of the previous assignee' do - is_expected.to have_body_text /#{previous_assignee.name}/ + is_expected.to have_body_text previous_assignee.name end it 'contains the name of the new assignee' do - is_expected.to have_body_text /#{assignee.name}/ + is_expected.to have_body_text assignee.name end it 'contains a link to the merge request' do - is_expected.to have_body_text /#{namespace_project_merge_request_path project.namespace, project, merge_request}/ + is_expected.to have_body_text namespace_project_merge_request_path(project.namespace, project, merge_request) end end @@ -301,15 +310,15 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/ + is_expected.to have_referable_subject(merge_request, reply: true) end it 'contains the names of the added labels' do - is_expected.to have_body_text /foo, bar, and baz/ + is_expected.to have_body_text 'foo, bar, and baz' end it 'contains a link to the merge request' do - is_expected.to have_body_text /#{namespace_project_merge_request_path project.namespace, project, merge_request}/ + is_expected.to have_body_text namespace_project_merge_request_path(project.namespace, project, merge_request) end end @@ -330,19 +339,19 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/i + is_expected.to have_referable_subject(merge_request, reply: true) end it 'contains the new status' do - is_expected.to have_body_text /#{status}/i + is_expected.to have_body_text status end it 'contains the user name' do - is_expected.to have_body_text /#{current_user.name}/i + is_expected.to have_body_text current_user.name end it 'contains a link to the merge request' do - is_expected.to have_body_text /#{namespace_project_merge_request_path project.namespace, project, merge_request}/ + is_expected.to have_body_text namespace_project_merge_request_path(project.namespace, project, merge_request) end end @@ -363,15 +372,15 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/ + is_expected.to have_referable_subject(merge_request, reply: true) end it 'contains the new status' do - is_expected.to have_body_text /merged/i + is_expected.to have_body_text 'merged' end it 'contains a link to the merge request' do - is_expected.to have_body_text /#{namespace_project_merge_request_path project.namespace, project, merge_request}/ + is_expected.to have_body_text namespace_project_merge_request_path(project.namespace, project, merge_request) end end end @@ -387,15 +396,15 @@ describe Notify do it_behaves_like "a user cannot unsubscribe through footer link" it 'has the correct subject' do - is_expected.to have_subject /Project was moved/ + is_expected.to have_subject "#{project.name} | Project was moved" end it 'contains name of project' do - is_expected.to have_body_text /#{project.name_with_namespace}/ + is_expected.to have_body_text project.name_with_namespace end it 'contains new user role' do - is_expected.to have_body_text /#{project.ssh_url_to_repo}/ + is_expected.to have_body_text project.ssh_url_to_repo end end @@ -424,9 +433,9 @@ describe Notify do expect(to_emails[0].address).to eq(project.members.owners_and_masters.first.user.notification_email) is_expected.to have_subject "Request to join the #{project.name_with_namespace} project" - is_expected.to have_body_text /#{project.name_with_namespace}/ - is_expected.to have_body_text /#{namespace_project_project_members_url(project.namespace, project)}/ - is_expected.to have_body_text /#{project_member.human_access}/ + is_expected.to have_body_text project.name_with_namespace + is_expected.to have_body_text namespace_project_project_members_url(project.namespace, project) + is_expected.to have_body_text project_member.human_access end end @@ -451,9 +460,9 @@ describe Notify do expect(to_emails[0].address).to eq(group.members.owners_and_masters.first.user.notification_email) is_expected.to have_subject "Request to join the #{project.name_with_namespace} project" - is_expected.to have_body_text /#{project.name_with_namespace}/ - is_expected.to have_body_text /#{namespace_project_project_members_url(project.namespace, project)}/ - is_expected.to have_body_text /#{project_member.human_access}/ + is_expected.to have_body_text project.name_with_namespace + is_expected.to have_body_text namespace_project_project_members_url(project.namespace, project) + is_expected.to have_body_text project_member.human_access end end end @@ -473,8 +482,8 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject "Access to the #{project.name_with_namespace} project was denied" - is_expected.to have_body_text /#{project.name_with_namespace}/ - is_expected.to have_body_text /#{project.web_url}/ + is_expected.to have_body_text project.name_with_namespace + is_expected.to have_body_text project.web_url end end @@ -490,9 +499,9 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject "Access to the #{project.name_with_namespace} project was granted" - is_expected.to have_body_text /#{project.name_with_namespace}/ - is_expected.to have_body_text /#{project.web_url}/ - is_expected.to have_body_text /#{project_member.human_access}/ + is_expected.to have_body_text project.name_with_namespace + is_expected.to have_body_text project.web_url + is_expected.to have_body_text project_member.human_access end end @@ -521,10 +530,10 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject "Invitation to join the #{project.name_with_namespace} project" - is_expected.to have_body_text /#{project.name_with_namespace}/ - is_expected.to have_body_text /#{project.web_url}/ - is_expected.to have_body_text /#{project_member.human_access}/ - is_expected.to have_body_text /#{project_member.invite_token}/ + is_expected.to have_body_text project.name_with_namespace + is_expected.to have_body_text project.web_url + is_expected.to have_body_text project_member.human_access + is_expected.to have_body_text project_member.invite_token end end @@ -546,10 +555,10 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject 'Invitation accepted' - is_expected.to have_body_text /#{project.name_with_namespace}/ - is_expected.to have_body_text /#{project.web_url}/ - is_expected.to have_body_text /#{project_member.invite_email}/ - is_expected.to have_body_text /#{invited_user.name}/ + is_expected.to have_body_text project.name_with_namespace + is_expected.to have_body_text project.web_url + is_expected.to have_body_text project_member.invite_email + is_expected.to have_body_text invited_user.name end end @@ -570,9 +579,9 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject 'Invitation declined' - is_expected.to have_body_text /#{project.name_with_namespace}/ - is_expected.to have_body_text /#{project.web_url}/ - is_expected.to have_body_text /#{project_member.invite_email}/ + is_expected.to have_body_text project.name_with_namespace + is_expected.to have_body_text project.web_url + is_expected.to have_body_text project_member.invite_email end end @@ -598,11 +607,11 @@ describe Notify do end it 'contains the message from the note' do - is_expected.to have_body_text /#{note.note}/ + is_expected.to have_body_text note.note end it 'does not contain note author' do - is_expected.not_to have_body_text /wrote\:/ + is_expected.not_to have_body_text 'wrote:' end context 'when enabled email_author_in_body' do @@ -612,7 +621,7 @@ describe Notify do it 'contains a link to note author' do is_expected.to have_body_text note.author_name - is_expected.to have_body_text /wrote\:/ + is_expected.to have_body_text 'wrote:' end end end @@ -632,7 +641,7 @@ describe Notify do it_behaves_like 'a user cannot unsubscribe through footer link' it 'has the correct subject' do - is_expected.to have_subject /Re: #{project.name} | #{commit.title} \(#{commit.short_id}\)/ + is_expected.to have_subject "Re: #{project.name} | #{commit.title.strip} (#{commit.short_id})" end it 'contains a link to the commit' do @@ -655,11 +664,11 @@ describe Notify do it_behaves_like 'an unsubscribeable thread' it 'has the correct subject' do - is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/ + is_expected.to have_referable_subject(merge_request, reply: true) end it 'contains a link to the merge request note' do - is_expected.to have_body_text /#{note_on_merge_request_path}/ + is_expected.to have_body_text note_on_merge_request_path end end @@ -678,11 +687,11 @@ describe Notify do it_behaves_like 'an unsubscribeable thread' it 'has the correct subject' do - is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/ + is_expected.to have_referable_subject(issue, reply: true) end it 'contains a link to the issue note' do - is_expected.to have_body_text /#{note_on_issue_path}/ + is_expected.to have_body_text note_on_issue_path end end end @@ -698,11 +707,11 @@ describe Notify do let(:note) { create(model, project: project, author: note_author) } it "includes diffs with character-level highlighting" do - is_expected.to have_body_text /}<\/span><\/span>/ + is_expected.to have_body_text '}' end it 'contains a link to the diff file' do - is_expected.to have_body_text /#{note.diff_file.file_path}/ + is_expected.to have_body_text note.diff_file.file_path end it_behaves_like 'it should have Gmail Actions links' @@ -718,11 +727,11 @@ describe Notify do end it 'contains the message from the note' do - is_expected.to have_body_text /#{note.note}/ + is_expected.to have_body_text note.note end it 'does not contain note author' do - is_expected.not_to have_body_text /wrote\:/ + is_expected.not_to have_body_text 'wrote:' end context 'when enabled email_author_in_body' do @@ -732,7 +741,7 @@ describe Notify do it 'contains a link to note author' do is_expected.to have_body_text note.author_name - is_expected.to have_body_text /wrote\:/ + is_expected.to have_body_text 'wrote:' end end end @@ -777,9 +786,9 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject "Request to join the #{group.name} group" - is_expected.to have_body_text /#{group.name}/ - is_expected.to have_body_text /#{group_group_members_url(group)}/ - is_expected.to have_body_text /#{group_member.human_access}/ + is_expected.to have_body_text group.name + is_expected.to have_body_text group_group_members_url(group) + is_expected.to have_body_text group_member.human_access end end @@ -798,8 +807,8 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject "Access to the #{group.name} group was denied" - is_expected.to have_body_text /#{group.name}/ - is_expected.to have_body_text /#{group.web_url}/ + is_expected.to have_body_text group.name + is_expected.to have_body_text group.web_url end end @@ -816,9 +825,9 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject "Access to the #{group.name} group was granted" - is_expected.to have_body_text /#{group.name}/ - is_expected.to have_body_text /#{group.web_url}/ - is_expected.to have_body_text /#{group_member.human_access}/ + is_expected.to have_body_text group.name + is_expected.to have_body_text group.web_url + is_expected.to have_body_text group_member.human_access end end @@ -847,10 +856,10 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject "Invitation to join the #{group.name} group" - is_expected.to have_body_text /#{group.name}/ - is_expected.to have_body_text /#{group.web_url}/ - is_expected.to have_body_text /#{group_member.human_access}/ - is_expected.to have_body_text /#{group_member.invite_token}/ + is_expected.to have_body_text group.name + is_expected.to have_body_text group.web_url + is_expected.to have_body_text group_member.human_access + is_expected.to have_body_text group_member.invite_token end end @@ -872,10 +881,10 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject 'Invitation accepted' - is_expected.to have_body_text /#{group.name}/ - is_expected.to have_body_text /#{group.web_url}/ - is_expected.to have_body_text /#{group_member.invite_email}/ - is_expected.to have_body_text /#{invited_user.name}/ + is_expected.to have_body_text group.name + is_expected.to have_body_text group.web_url + is_expected.to have_body_text group_member.invite_email + is_expected.to have_body_text invited_user.name end end @@ -896,9 +905,9 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject 'Invitation declined' - is_expected.to have_body_text /#{group.name}/ - is_expected.to have_body_text /#{group.web_url}/ - is_expected.to have_body_text /#{group_member.invite_email}/ + is_expected.to have_body_text group.name + is_expected.to have_body_text group.web_url + is_expected.to have_body_text group_member.invite_email end end end @@ -925,11 +934,11 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /^Confirmation instructions/ + is_expected.to have_subject 'Confirmation instructions | A Nice Suffix' end it 'includes a link to the site' do - is_expected.to have_body_text /#{example_site_path}/ + is_expected.to have_body_text example_site_path end end @@ -952,11 +961,11 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /Pushed new branch master/ + is_expected.to have_subject "[Git][#{project.full_path}] Pushed new branch master" end it 'contains a link to the branch' do - is_expected.to have_body_text /#{tree_path}/ + is_expected.to have_body_text tree_path end end @@ -979,11 +988,11 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /Pushed new tag v1\.0/ + is_expected.to have_subject "[Git][#{project.full_path}] Pushed new tag v1.0" end it 'contains a link to the tag' do - is_expected.to have_body_text /#{tree_path}/ + is_expected.to have_body_text tree_path end end @@ -1005,7 +1014,7 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /Deleted branch master/ + is_expected.to have_subject "[Git][#{project.full_path}] Deleted branch master" end end @@ -1027,7 +1036,7 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /Deleted tag v1\.0/ + is_expected.to have_subject "[Git][#{project.full_path}] Deleted tag v1.0" end end @@ -1055,23 +1064,23 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /\[#{project.path_with_namespace}\]\[master\] #{commits.length} commits:/ + is_expected.to have_subject "[Git][#{project.full_path}][master] #{commits.length} commits: Ruby files modified" end it 'includes commits list' do - is_expected.to have_body_text /Change some files/ + is_expected.to have_body_text 'Change some files' end it 'includes diffs with character-level highlighting' do - is_expected.to have_body_text /def<\/span> archive_formats_regex/ + is_expected.to have_body_text 'def archive_formats_regex' end it 'contains a link to the diff' do - is_expected.to have_body_text /#{diff_path}/ + is_expected.to have_body_text diff_path end it 'does not contain the misleading footer' do - is_expected.not_to have_body_text /you are a member of/ + is_expected.not_to have_body_text 'you are a member of' end context "when set to send from committer email if domain matches" do @@ -1157,19 +1166,19 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /#{commits.first.title}/ + is_expected.to have_subject "[Git][#{project.full_path}][master] #{commits.first.title}" end it 'includes commits list' do - is_expected.to have_body_text /Change some files/ + is_expected.to have_body_text 'Change some files' end it 'includes diffs with character-level highlighting' do - is_expected.to have_body_text /def<\/span> archive_formats_regex/ + is_expected.to have_body_text 'def archive_formats_regex' end it 'contains a link to the diff' do - is_expected.to have_body_text /#{diff_path}/ + is_expected.to have_body_text diff_path end end -- cgit v1.2.1 From 29e34c332687be9456578a9b5f60adb10f4e10b5 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Tue, 7 Mar 2017 18:04:44 +0200 Subject: Preserve order by priority on issues board --- app/models/issue.rb | 7 +++++++ app/services/boards/issues/list_service.rb | 2 +- spec/models/issue_spec.rb | 15 ++++++++++++++ spec/services/boards/issues/list_service_spec.rb | 26 ++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 1 deletion(-) diff --git a/app/models/issue.rb b/app/models/issue.rb index 0f7a26ee3e1..dba9398a43c 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -96,6 +96,13 @@ class Issue < ActiveRecord::Base end end + def self.order_by_position_and_priority + order_labels_priority. + reorder(Gitlab::Database.nulls_last_order('relative_position', 'ASC'), + Gitlab::Database.nulls_last_order('highest_priority', 'ASC'), + "id DESC") + end + # `from` argument can be a Namespace or Project. def to_reference(from = nil, full: false) reference = "#{self.class.reference_prefix}#{iid}" diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb index 185838764c1..83f51947bd4 100644 --- a/app/services/boards/issues/list_service.rb +++ b/app/services/boards/issues/list_service.rb @@ -5,7 +5,7 @@ module Boards issues = IssuesFinder.new(current_user, filter_params).execute issues = without_board_labels(issues) unless movable_list? issues = with_list_label(issues) if movable_list? - issues.reorder(Gitlab::Database.nulls_last_order('relative_position', 'ASC')) + issues.order_by_position_and_priority end private diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index bba9058f394..f67fbe79bde 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -22,6 +22,21 @@ describe Issue, models: true do it { is_expected.to have_db_index(:deleted_at) } end + describe '#order_by_position_and_priority' do + let(:project) { create :empty_project } + let(:p1) { create(:label, title: 'P1', project: project, priority: 1) } + let(:p2) { create(:label, title: 'P2', project: project, priority: 2) } + let!(:issue1) { create(:labeled_issue, project: project, labels: [p1]) } + let!(:issue2) { create(:labeled_issue, project: project, labels: [p2]) } + let!(:issue3) { create(:issue, project: project, relative_position: 100) } + let!(:issue4) { create(:issue, project: project, relative_position: 200) } + + it 'returns ordered list' do + expect(project.issues.order_by_position_and_priority). + to match [issue3, issue4, issue1, issue2] + end + end + describe '#to_reference' do let(:namespace) { build(:namespace, path: 'sample-namespace') } let(:project) { build(:empty_project, name: 'sample-project', namespace: namespace) } diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb index 01baedc4761..22115c6566d 100644 --- a/spec/services/boards/issues/list_service_spec.rb +++ b/spec/services/boards/issues/list_service_spec.rb @@ -43,6 +43,32 @@ describe Boards::Issues::ListService, services: true do described_class.new(project, user, params).execute end + context 'issues are ordered by priority' do + it 'returns opened issues when list_id is missing' do + params = { board_id: board.id } + + issues = described_class.new(project, user, params).execute + + expect(issues).to eq [opened_issue2, reopened_issue1, opened_issue1] + end + + it 'returns closed issues when listing issues from Done' do + params = { board_id: board.id, id: done.id } + + issues = described_class.new(project, user, params).execute + + expect(issues).to eq [closed_issue4, closed_issue2, closed_issue3, closed_issue1] + end + + it 'returns opened issues that have label list applied when listing issues from a label list' do + params = { board_id: board.id, id: list1.id } + + issues = described_class.new(project, user, params).execute + + expect(issues).to eq [list1_issue3, list1_issue1, list1_issue2] + end + end + context 'with list that does not belong to the board' do it 'raises an error' do list = create(:list) -- cgit v1.2.1 From 3e29936a15eb2f6c9bc2d92ebf1a67a0cadb916e Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 14 Mar 2017 01:24:56 -0500 Subject: Fix first line markdown helper for user profile activity stream Fix https://gitlab.com/gitlab-org/gitlab-ce/issues/29425 --- app/helpers/events_helper.rb | 4 ++-- app/helpers/gitlab_markdown_helper.rb | 2 +- spec/helpers/gitlab_markdown_helper_spec.rb | 20 ++++++++++++++++++-- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 5605393c0c3..fb872a13f74 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -165,8 +165,8 @@ module EventsHelper sanitize( text, - tags: %w(a img b pre code p span), - attributes: Rails::Html::WhiteListSanitizer.allowed_attributes + ['style'] + tags: %w(a img gl-emoji b pre code p span), + attributes: Rails::Html::WhiteListSanitizer.allowed_attributes + ['style', 'data-name', 'data-unicode-version'] ) end diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 6d365ea9251..6226cfe25cf 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -172,7 +172,7 @@ module GitlabMarkdownHelper # text hasn't already been truncated, then append "..." to the node contents # and return true. Otherwise return false. def truncate_if_block(node, truncated) - if node.element? && node.description.block? && !truncated + if node.element? && node.description&.block? && !truncated node.inner_html = "#{node.inner_html}..." if node.next_sibling true else diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 9ffd4b9371c..6cf3f86680a 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -152,9 +152,8 @@ describe GitlabMarkdownHelper do end describe '#first_line_in_markdown' do - let(:text) { "@#{user.username}, can you look at this?\nHello world\n"} - it 'truncates Markdown properly' do + text = "@#{user.username}, can you look at this?\nHello world\n" actual = first_line_in_markdown(text, 100, project: project) doc = Nokogiri::HTML.parse(actual) @@ -169,6 +168,23 @@ describe GitlabMarkdownHelper do expect(doc.content).to eq "@#{user.username}, can you look at this?..." end + + it 'truncates Markdown with emoji properly' do + text = "foo :wink:\nbar :grinning:" + actual = first_line_in_markdown(text, 100, project: project) + + doc = Nokogiri::HTML.parse(actual) + + # Make sure we didn't create invalid markup + # But also account for the 2 errors caused by the unknown `gl-emoji` elements + expect(doc.errors.length).to eq(2) + + expect(doc.css('gl-emoji').length).to eq(2) + expect(doc.css('gl-emoji')[0].attr('data-name')).to eq 'wink' + expect(doc.css('gl-emoji')[1].attr('data-name')).to eq 'grinning' + + expect(doc.content).to eq "foo 😉\nbar 😀" + end end describe '#cross_project_reference' do -- cgit v1.2.1 From 8daa641e4d6d5dd2fab78abd851b0e8899d4ea11 Mon Sep 17 00:00:00 2001 From: Raveesh Date: Tue, 14 Mar 2017 12:50:32 -0400 Subject: Switch to using milestone.to_reference when displaying milestone Fix #29214 --- app/views/projects/milestones/edit.html.haml | 2 +- app/views/projects/milestones/show.html.haml | 2 +- changelogs/unreleased/fix-milestone-name-on-show.yml | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/fix-milestone-name-on-show.yml diff --git a/app/views/projects/milestones/edit.html.haml b/app/views/projects/milestones/edit.html.haml index 11f41e75e63..55b0b837c6d 100644 --- a/app/views/projects/milestones/edit.html.haml +++ b/app/views/projects/milestones/edit.html.haml @@ -5,7 +5,7 @@ %div{ class: container_class } %h3.page-title - Edit Milestone ##{@milestone.iid} + Edit Milestone #{@milestone.to_reference} %hr diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index b4dde2c86c9..d16f49bd33a 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -20,7 +20,7 @@ .header-text-content %span.identifier %strong - Milestone %#{@milestone.iid} + Milestone #{@milestone.to_reference} - if @milestone.due_date || @milestone.start_date = milestone_date_range(@milestone) .milestone-buttons diff --git a/changelogs/unreleased/fix-milestone-name-on-show.yml b/changelogs/unreleased/fix-milestone-name-on-show.yml new file mode 100644 index 00000000000..bf17a758c80 --- /dev/null +++ b/changelogs/unreleased/fix-milestone-name-on-show.yml @@ -0,0 +1,4 @@ +--- +title: Fix Milestone name on show page +merge_request: +author: Raveesh -- cgit v1.2.1 From f97c1d1001a1c16ab51fb62723f30f6ffa467d4f Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 14 Mar 2017 11:47:05 -0500 Subject: Fix link togglers jumping to top Fix #29414 --- app/assets/javascripts/behaviors/toggler_behavior.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js index 0726c6c9636..92f3bb3ff52 100644 --- a/app/assets/javascripts/behaviors/toggler_behavior.js +++ b/app/assets/javascripts/behaviors/toggler_behavior.js @@ -21,8 +21,13 @@ // %a.js-toggle-button // %div.js-toggle-content // - $('body').on('click', '.js-toggle-button', function() { + $('body').on('click', '.js-toggle-button', function(e) { toggleContainer($(this).closest('.js-toggle-container')); + + const targetTag = e.target.tagName.toLowerCase(); + if (targetTag === 'a' || targetTag === 'button') { + e.preventDefault(); + } }); // If we're accessing a permalink, ensure it is not inside a -- cgit v1.2.1 From 74ec81a4f3ba3a98946e00fd08bd72567e338271 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 14 Mar 2017 11:25:24 +0100 Subject: Bump pages daemon to 0.4.0 [ci skip] --- doc/administration/pages/source.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/administration/pages/source.md b/doc/administration/pages/source.md index b4588f8b43c..a45c3306457 100644 --- a/doc/administration/pages/source.md +++ b/doc/administration/pages/source.md @@ -105,7 +105,7 @@ The Pages daemon doesn't listen to the outside world. cd /home/git sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git cd gitlab-pages - sudo -u git -H git checkout v0.3.2 + sudo -u git -H git checkout v$( Date: Tue, 14 Mar 2017 11:56:15 -0500 Subject: Include time tracking attributes in webhooks payload --- app/models/issue.rb | 8 +++++++- app/models/merge_request.rb | 5 ++++- .../unreleased/27271-missing-time-spent-in-issue-webhook.yml | 4 ++++ spec/models/issue_spec.rb | 11 +++++++++++ spec/models/merge_request_spec.rb | 6 +++++- 5 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/27271-missing-time-spent-in-issue-webhook.yml diff --git a/app/models/issue.rb b/app/models/issue.rb index 0f7a26ee3e1..2cc237635f9 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -58,7 +58,13 @@ class Issue < ActiveRecord::Base end def hook_attrs - attributes + attrs = { + total_time_spent: total_time_spent, + human_total_time_spent: human_total_time_spent, + human_time_estimate: human_time_estimate + } + + attributes.merge!(attrs) end def self.reference_prefix diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 0f7b8311588..4759829a15c 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -523,7 +523,10 @@ class MergeRequest < ActiveRecord::Base source: source_project.try(:hook_attrs), target: target_project.hook_attrs, last_commit: nil, - work_in_progress: work_in_progress? + work_in_progress: work_in_progress?, + total_time_spent: total_time_spent, + human_total_time_spent: human_total_time_spent, + human_time_estimate: human_time_estimate } if diff_head_commit diff --git a/changelogs/unreleased/27271-missing-time-spent-in-issue-webhook.yml b/changelogs/unreleased/27271-missing-time-spent-in-issue-webhook.yml new file mode 100644 index 00000000000..4ea52a70e89 --- /dev/null +++ b/changelogs/unreleased/27271-missing-time-spent-in-issue-webhook.yml @@ -0,0 +1,4 @@ +--- +title: Include time tracking attributes in webhooks payload +merge_request: 9942 +author: diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index bba9058f394..898a9c8da35 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -620,4 +620,15 @@ describe Issue, models: true do end end end + + describe '#hook_attrs' do + let(:attrs_hash) { subject.hook_attrs } + + it 'includes time tracking attrs' do + expect(attrs_hash).to include(:total_time_spent) + expect(attrs_hash).to include(:human_time_estimate) + expect(attrs_hash).to include(:human_total_time_spent) + expect(attrs_hash).to include('time_estimate') + end + end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index fcaf4c71182..24e7c1b17d9 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -542,7 +542,7 @@ describe MergeRequest, models: true do end describe "#hook_attrs" do - let(:attrs_hash) { subject.hook_attrs.to_h } + let(:attrs_hash) { subject.hook_attrs } [:source, :target].each do |key| describe "#{key} key" do @@ -558,6 +558,10 @@ describe MergeRequest, models: true do expect(attrs_hash).to include(:target) expect(attrs_hash).to include(:last_commit) expect(attrs_hash).to include(:work_in_progress) + expect(attrs_hash).to include(:total_time_spent) + expect(attrs_hash).to include(:human_time_estimate) + expect(attrs_hash).to include(:human_total_time_spent) + expect(attrs_hash).to include('time_estimate') end end -- cgit v1.2.1 From c9abdadd7a08f972d5a12472f9f5ac443e37a6ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 14 Mar 2017 18:08:50 +0100 Subject: Ensure dots in project path is allowed in the commits API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- lib/api/commits.rb | 2 +- lib/api/v3/commits.rb | 2 +- spec/requests/api/commits_spec.rb | 17 +++++++++-------- spec/requests/api/v3/commits_spec.rb | 15 ++++++++------- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 42401abfe0f..48939798900 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -10,7 +10,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects do + resource :projects, requirements: { id: /.+/ } do desc 'Get a project repository commits' do success Entities::RepoCommit end diff --git a/lib/api/v3/commits.rb b/lib/api/v3/commits.rb index d254d247042..6f36b2bc1c4 100644 --- a/lib/api/v3/commits.rb +++ b/lib/api/v3/commits.rb @@ -11,7 +11,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects do + resource :projects, requirements: { id: /.+/ } do desc 'Get a project repository commits' do success ::API::Entities::RepoCommit end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 585449e62b6..7c0f2fb9fe9 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -178,7 +178,7 @@ describe API::Commits, api: true do end end - describe "Create a commit with multiple files and actions" do + describe "POST /projects/:id/repository/commits" do let!(:url) { "/projects/#{project.id}/repository/commits" } it 'returns a 403 unauthorized for user without permissions' do @@ -193,7 +193,7 @@ describe API::Commits, api: true do expect(response).to have_http_status(400) end - context :create do + describe 'create' do let(:message) { 'Created file' } let!(:invalid_c_params) do { @@ -237,8 +237,9 @@ describe API::Commits, api: true do expect(response).to have_http_status(400) end - context 'with project path in URL' do - let(:url) { "/projects/#{project.full_path.gsub('/', '%2F')}/repository/commits" } + context 'with project path containing a dot in URL' do + let!(:user) { create(:user, username: 'foo.bar') } + let(:url) { "/projects/#{CGI.escape(project.full_path)}/repository/commits" } it 'a new file in project repo' do post api(url, user), valid_c_params @@ -248,7 +249,7 @@ describe API::Commits, api: true do end end - context :delete do + describe 'delete' do let(:message) { 'Deleted file' } let!(:invalid_d_params) do { @@ -289,7 +290,7 @@ describe API::Commits, api: true do end end - context :move do + describe 'move' do let(:message) { 'Moved file' } let!(:invalid_m_params) do { @@ -334,7 +335,7 @@ describe API::Commits, api: true do end end - context :update do + describe 'update' do let(:message) { 'Updated file' } let!(:invalid_u_params) do { @@ -377,7 +378,7 @@ describe API::Commits, api: true do end end - context "multiple operations" do + describe 'multiple operations' do let(:message) { 'Multiple actions' } let!(:invalid_mo_params) do { diff --git a/spec/requests/api/v3/commits_spec.rb b/spec/requests/api/v3/commits_spec.rb index e298ef055e1..adba3a787aa 100644 --- a/spec/requests/api/v3/commits_spec.rb +++ b/spec/requests/api/v3/commits_spec.rb @@ -88,7 +88,7 @@ describe API::V3::Commits, api: true do end end - describe "Create a commit with multiple files and actions" do + describe "POST /projects/:id/repository/commits" do let!(:url) { "/projects/#{project.id}/repository/commits" } it 'returns a 403 unauthorized for user without permissions' do @@ -103,7 +103,7 @@ describe API::V3::Commits, api: true do expect(response).to have_http_status(400) end - context :create do + describe 'create' do let(:message) { 'Created file' } let!(:invalid_c_params) do { @@ -147,8 +147,9 @@ describe API::V3::Commits, api: true do expect(response).to have_http_status(400) end - context 'with project path in URL' do - let(:url) { "/projects/#{project.full_path.gsub('/', '%2F')}/repository/commits" } + context 'with project path containing a dot in URL' do + let!(:user) { create(:user, username: 'foo.bar') } + let(:url) { "/projects/#{CGI.escape(project.full_path)}/repository/commits" } it 'a new file in project repo' do post v3_api(url, user), valid_c_params @@ -158,7 +159,7 @@ describe API::V3::Commits, api: true do end end - context :delete do + describe 'delete' do let(:message) { 'Deleted file' } let!(:invalid_d_params) do { @@ -199,7 +200,7 @@ describe API::V3::Commits, api: true do end end - context :move do + describe 'move' do let(:message) { 'Moved file' } let!(:invalid_m_params) do { @@ -244,7 +245,7 @@ describe API::V3::Commits, api: true do end end - context :update do + describe 'update' do let(:message) { 'Updated file' } let!(:invalid_u_params) do { -- cgit v1.2.1 From 316e0edc5ae0ecec17aa7e15b3c8b0743a216195 Mon Sep 17 00:00:00 2001 From: gpongelli Date: Tue, 14 Mar 2017 17:14:35 +0000 Subject: Syshook documentation updated --- doc/system_hooks/system_hooks.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md index ec13c2446ef..ad5ffc84473 100644 --- a/doc/system_hooks/system_hooks.md +++ b/doc/system_hooks/system_hooks.md @@ -313,8 +313,19 @@ X-Gitlab-Event: System Hook "git_ssh_url":"git@example.com:mike/diaspora.git", "visibility_level":0 }, - "commits": [], - "total_commits_count": 0 + "commits": [ + { + "id": "c5feabde2d8cd023215af4d2ceeb7a64839fc428", + "message": "Add simple search to projects in public area", + "timestamp": "2013-05-13T18:18:08+00:00", + "url": "https://dev.gitlab.org/gitlab/gitlabhq/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428", + "author": { + "name": "Dmitriy Zaporozhets", + "email": "dmitriy.zaporozhets@gmail.com" + } + } + ], + "total_commits_count": 1 } ``` -- cgit v1.2.1 From 464ca33747ec3f2a4063795e18ab5888e429b334 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 14 Mar 2017 18:30:53 +0100 Subject: Allow to override GITLAB_GIT_TEST_REPO_URL to specify a different gitlab-git-test repo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We will set this to the dev mirror in GitLab CE and EE on dev. Signed-off-by: Rémy Coutable --- spec/lib/gitlab/git/repository_spec.rb | 2 +- spec/support/seed_helper.rb | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index bc139d5ef28..9c3a4571ce4 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -507,7 +507,7 @@ describe Gitlab::Git::Repository, seed_helper: true do describe "#remote_add" do before(:all) do @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH) - @repo.remote_add("new_remote", SeedHelper::GITLAB_URL) + @repo.remote_add("new_remote", SeedHelper::GITLAB_GIT_TEST_REPO_URL) end it "should add the remote" do diff --git a/spec/support/seed_helper.rb b/spec/support/seed_helper.rb index 07f81e9c4f3..f55fee28ff9 100644 --- a/spec/support/seed_helper.rb +++ b/spec/support/seed_helper.rb @@ -7,7 +7,7 @@ TEST_MUTABLE_REPO_PATH = File.join(SEED_REPOSITORY_PATH, "mutable-repo.git") TEST_BROKEN_REPO_PATH = File.join(SEED_REPOSITORY_PATH, "broken-repo.git") module SeedHelper - GITLAB_URL = "https://gitlab.com/gitlab-org/gitlab-git-test.git".freeze + GITLAB_GIT_TEST_REPO_URL = ENV.fetch('GITLAB_GIT_TEST_REPO_URL', 'https://gitlab.com/gitlab-org/gitlab-git-test.git').freeze def ensure_seeds if File.exist?(SEED_REPOSITORY_PATH) @@ -25,7 +25,7 @@ module SeedHelper end def create_bare_seeds - system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{GITLAB_URL}), + system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{GITLAB_GIT_TEST_REPO_URL}), chdir: SEED_REPOSITORY_PATH, out: '/dev/null', err: '/dev/null') @@ -45,7 +45,7 @@ module SeedHelper system(git_env, *%w(git branch -t feature origin/feature), chdir: TEST_MUTABLE_REPO_PATH, out: '/dev/null', err: '/dev/null') - system(git_env, *%W(#{Gitlab.config.git.bin_path} remote add expendable #{GITLAB_URL}), + system(git_env, *%W(#{Gitlab.config.git.bin_path} remote add expendable #{GITLAB_GIT_TEST_REPO_URL}), chdir: TEST_MUTABLE_REPO_PATH, out: '/dev/null', err: '/dev/null') end -- cgit v1.2.1 From ee2ddd059520f2c9a875c888a2c4eb44af3643a5 Mon Sep 17 00:00:00 2001 From: Jose Ivan Vargas Date: Fri, 10 Mar 2017 18:02:35 -0600 Subject: Moved the gear settings dropdown in the group view to a tab --- app/views/groups/_settings_head.html.haml | 14 ++++++++++++++ app/views/groups/edit.html.haml | 1 + app/views/groups/projects.html.haml | 1 + app/views/layouts/nav/_group.html.haml | 9 ++++++++- app/views/layouts/nav/_group_settings.html.haml | 18 ------------------ .../unreleased/group-gear-setting-dropdown-to-tab.yml | 4 ++++ 6 files changed, 28 insertions(+), 19 deletions(-) create mode 100644 app/views/groups/_settings_head.html.haml delete mode 100644 app/views/layouts/nav/_group_settings.html.haml create mode 100644 changelogs/unreleased/group-gear-setting-dropdown-to-tab.yml diff --git a/app/views/groups/_settings_head.html.haml b/app/views/groups/_settings_head.html.haml new file mode 100644 index 00000000000..dc11efeb0c4 --- /dev/null +++ b/app/views/groups/_settings_head.html.haml @@ -0,0 +1,14 @@ += content_for :sub_nav do + .scrolling-tabs-container.sub-nav-scroll + = render 'shared/nav_scroll' + .nav-links.sub-nav.scrolling-tabs + %ul{ class: container_class } + = nav_link(path: 'groups#projects') do + = link_to projects_group_path(@group), title: 'Projects' do + %span + Projects + + = nav_link(path: 'groups#edit') do + = link_to edit_group_path(@group), title: 'Edit Group' do + %span + Edit Group \ No newline at end of file diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 2706e8692d1..80a77dab97f 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -1,3 +1,4 @@ += render "groups/settings_head" .panel.panel-default.prepend-top-default .panel-heading Group settings diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml index 2e7e5e5c309..1f4a3e2a829 100644 --- a/app/views/groups/projects.html.haml +++ b/app/views/groups/projects.html.haml @@ -1,4 +1,5 @@ - page_title "Projects" += render "groups/settings_head" .panel.panel-default.prepend-top-default .panel-heading diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index a6e96942021..9de0e344196 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -1,4 +1,5 @@ -= render 'layouts/nav/group_settings' +- can_admin_group = can?(current_user, :admin_group, @group) +- can_edit = can?(current_user, :admin_group, @group) .scrolling-tabs-container{ class: nav_control_class } .fade-left = icon('angle-left') @@ -25,3 +26,9 @@ = link_to group_group_members_path(@group), title: 'Members' do %span Members + - if current_user + - if can_admin_group || can_edit + = nav_link(path: %w[groups#projects groups#edit]) do + = link_to projects_group_path(@group), title: 'Settings' do + %span + Settings diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml deleted file mode 100644 index 30feb6813b4..00000000000 --- a/app/views/layouts/nav/_group_settings.html.haml +++ /dev/null @@ -1,18 +0,0 @@ -- if current_user - - can_admin_group = can?(current_user, :admin_group, @group) - - can_edit = can?(current_user, :admin_group, @group) - - - if can_admin_group || can_edit - .controls - .dropdown.group-settings-dropdown - %a.dropdown-new.btn.btn-default#group-settings-button{ href: '#', 'data-toggle' => 'dropdown' } - = icon('cog') - = icon('caret-down') - %ul.dropdown-menu.dropdown-menu-align-right - - if can_admin_group - = nav_link(path: 'groups#projects') do - = link_to 'Projects', projects_group_path(@group), title: 'Projects' - - if can_edit && can_admin_group - %li.divider - %li - = link_to 'Edit Group', edit_group_path(@group) diff --git a/changelogs/unreleased/group-gear-setting-dropdown-to-tab.yml b/changelogs/unreleased/group-gear-setting-dropdown-to-tab.yml new file mode 100644 index 00000000000..aff1bdd957c --- /dev/null +++ b/changelogs/unreleased/group-gear-setting-dropdown-to-tab.yml @@ -0,0 +1,4 @@ +--- +title: Moved the gear settings dropdown to a tab in the groups view +merge_request: +author: -- cgit v1.2.1 From f47946591a52536c7dd7d02d11ffb7390549470b Mon Sep 17 00:00:00 2001 From: Jose Ivan Vargas Date: Fri, 10 Mar 2017 18:40:33 -0600 Subject: Fixed haml_lint warning for the settings_head partial --- app/views/groups/_settings_head.html.haml | 2 +- app/views/groups/projects.html.haml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/views/groups/_settings_head.html.haml b/app/views/groups/_settings_head.html.haml index dc11efeb0c4..d225f7ed3c0 100644 --- a/app/views/groups/_settings_head.html.haml +++ b/app/views/groups/_settings_head.html.haml @@ -11,4 +11,4 @@ = nav_link(path: 'groups#edit') do = link_to edit_group_path(@group), title: 'Edit Group' do %span - Edit Group \ No newline at end of file + Edit Group diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml index 1f4a3e2a829..83bdd654f27 100644 --- a/app/views/groups/projects.html.haml +++ b/app/views/groups/projects.html.haml @@ -1,4 +1,3 @@ -- page_title "Projects" = render "groups/settings_head" .panel.panel-default.prepend-top-default -- cgit v1.2.1 From 30f99608ffa5a4ce3d403276df5d68a23ec9b338 Mon Sep 17 00:00:00 2001 From: Jose Ivan Vargas Date: Tue, 14 Mar 2017 12:00:00 -0600 Subject: Fixed some missing permission conditions --- app/views/groups/_settings_head.html.haml | 11 +++++++---- app/views/layouts/nav/_group.html.haml | 12 +++++------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/app/views/groups/_settings_head.html.haml b/app/views/groups/_settings_head.html.haml index d225f7ed3c0..d99426bc2c1 100644 --- a/app/views/groups/_settings_head.html.haml +++ b/app/views/groups/_settings_head.html.haml @@ -1,3 +1,5 @@ +- can_admin_group = can?(current_user, :admin_group, @group) +- can_edit = can?(current_user, :admin_group, @group) = content_for :sub_nav do .scrolling-tabs-container.sub-nav-scroll = render 'shared/nav_scroll' @@ -8,7 +10,8 @@ %span Projects - = nav_link(path: 'groups#edit') do - = link_to edit_group_path(@group), title: 'Edit Group' do - %span - Edit Group + - if can_edit && can_admin_group + = nav_link(path: 'groups#edit') do + = link_to edit_group_path(@group), title: 'Edit Group' do + %span + Edit Group diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index 9de0e344196..b2ecf6504e0 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -1,5 +1,4 @@ - can_admin_group = can?(current_user, :admin_group, @group) -- can_edit = can?(current_user, :admin_group, @group) .scrolling-tabs-container{ class: nav_control_class } .fade-left = icon('angle-left') @@ -26,9 +25,8 @@ = link_to group_group_members_path(@group), title: 'Members' do %span Members - - if current_user - - if can_admin_group || can_edit - = nav_link(path: %w[groups#projects groups#edit]) do - = link_to projects_group_path(@group), title: 'Settings' do - %span - Settings + - if current_user && can_admin_group + = nav_link(path: %w[groups#projects groups#edit]) do + = link_to projects_group_path(@group), title: 'Settings' do + %span + Settings -- cgit v1.2.1 From bb99fc2572a796b938bd67128f6482c180e3942b Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 7 Mar 2017 21:32:36 +0100 Subject: Add nested groups documentation [ci skip] --- doc/user/group/subgroups/img/create_new_group.png | Bin 0 -> 18503 bytes .../group/subgroups/img/create_subgroup_button.png | Bin 0 -> 8402 bytes doc/user/group/subgroups/img/group_members.png | Bin 0 -> 48240 bytes doc/user/group/subgroups/img/mention_subgroups.png | Bin 0 -> 39666 bytes doc/user/group/subgroups/index.md | 153 +++++++++++++++++++++ doc/user/permissions.md | 1 + doc/workflow/README.md | 1 + 7 files changed, 155 insertions(+) create mode 100644 doc/user/group/subgroups/img/create_new_group.png create mode 100644 doc/user/group/subgroups/img/create_subgroup_button.png create mode 100644 doc/user/group/subgroups/img/group_members.png create mode 100644 doc/user/group/subgroups/img/mention_subgroups.png create mode 100644 doc/user/group/subgroups/index.md diff --git a/doc/user/group/subgroups/img/create_new_group.png b/doc/user/group/subgroups/img/create_new_group.png new file mode 100644 index 00000000000..9d011ec709a Binary files /dev/null and b/doc/user/group/subgroups/img/create_new_group.png differ diff --git a/doc/user/group/subgroups/img/create_subgroup_button.png b/doc/user/group/subgroups/img/create_subgroup_button.png new file mode 100644 index 00000000000..000b54c2855 Binary files /dev/null and b/doc/user/group/subgroups/img/create_subgroup_button.png differ diff --git a/doc/user/group/subgroups/img/group_members.png b/doc/user/group/subgroups/img/group_members.png new file mode 100644 index 00000000000..b95fe6263bf Binary files /dev/null and b/doc/user/group/subgroups/img/group_members.png differ diff --git a/doc/user/group/subgroups/img/mention_subgroups.png b/doc/user/group/subgroups/img/mention_subgroups.png new file mode 100644 index 00000000000..8e6bed0111b Binary files /dev/null and b/doc/user/group/subgroups/img/mention_subgroups.png differ diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md new file mode 100644 index 00000000000..9522e6fc4ba --- /dev/null +++ b/doc/user/group/subgroups/index.md @@ -0,0 +1,153 @@ +# Subgroups + +> [Introduced][ce-2772] in GitLab 9.0. + +With subgroups (also called nested groups or hierarchical groups) you can have +up to 20 levels of nested groups, which among other things can help you to: + +- **Separate internal / external organizations.** Since every group + can have its own visibility level, you are able to host groups for different + purposes under the same umbrella. +- **Organize large projects.** For large projects, subgroups makes it + potentially easier to separate permissions on parts of the source code. +- **Make it easier to manage people and control visibility.** Give people + different [permissions][] depending on their group [membership](#membership). + +## Overview + +A group can have many subgroups inside it, and at the same time a group can have +only 1 parent group. It resembles a directory behavior, like the one below: + +``` +group0 +└── subgroup01a +└── subgroup01b + └── subgroup02 + └── subgroup03 +``` + +In a real world example, imagine maintaining a GNU/Linux distribution with the +first group being the name of the distro and subsequent groups split like: + +``` +Organization Group - GNU/Linux distro + └── Category Subgroup - Packages + └── project - Package01 + └── project - Package02 + └── Category Subgroup - Software + └── project - Core + └── project - CLI + └── project - Android app + └── project - iOS app + └── Category Subgroup - Infra tools + └── project - Ansible playbooks +``` + +Another example of GitLab as a company would be the following: + +``` +Organization Group - GitLab + └── Category Subroup - Marketing + └── project - Design + └── project - General + └── Category Subgroup - Software + └── project - GitLab CE + └── project - GitLab EE + └── project - Omnibus GitLab + └── project - GitLab Runner + └── project - GitLab Pages daemon + └── Category Subgroup - Infra tools + └── project - Chef cookbooks + └── Category Subgroup - Executive team +``` + +--- + +The maximum nested groups a group can have, including the first one in the +hierarchy, is 21. + +Things like transferring or importing a project inside nested groups, work like +when performing these actions the traditional way with the `group/project` +structure. + +## Creating a subgroup + +>**Notes:** +- You need to be an Owner of a group in order to be able to create + a subgroup. For more information check the [permissions table][permissions]. +- For a list of words that are not allowed to be used as group names see the + [`namespace_validator.rb` file][reserved] under the `RESERVED` and + `WILDCARD_ROUTES` lists. + +To create a subgroup: + +1. In the group's dashboard go to the **Subgroups** page and click **Create subgroup**. + + ![Subgroups page](img/create_subgroup_button.png) + +1. Create a new group like you would normally do. Notice that the parent group + namespace is fixed under **Group path**. The visibility level can differ from + the parent group. + + ![Subgroups page](img/create_new_group.png) + +1. Click the **Create group** button and you will be taken to the new group's + dashboard page. + +--- + +You can follow the same process to create any subsequent groups. + +## Membership + +When you add a member to a subgroup, they inherit the membership and permission +level from the parent group. This model allows access to nested groups if you +have membership in one of its parents. + +You can tell if a member has inherited the permissions from a parent group by +looking at the group's **Members** page. + +![Group members page](img/group_members.png) + +From the image above, we can deduct the following things: + +- There are 5 members that have access to the group **four** +- Administrator is the Owner and member of all subgroups +- User0 is a Reporter and has inherited their permissions from group **one** + which is above the hierarchy of group **four** +- User1 is a Developer and has inherited their permissions from group + **one/two** which is above the hierarchy of group **four** +- User2 is a Developer and has inherited their permissions from group + **one/two/three** which is above the hierarchy of group **four** +- User3 is a Master of group **four**, there is no indication of a parent + group therefore they belong to group **four** + +The group permissions for a member can be changed only by Owners and only on +the **Members** page of the group the member was added. + +## Mentioning subgroups + +Mentioning groups (`@group`) in issues, commits and merge requests, would +mention all members of that group. Now with subgroups, there is a more granular +support if you want to split your group's structure. Mentioning works as before +and you can choose the group of people to be summoned. + +![Mentioning subgroups](img/mention_subgroups.png) + +## Limitations + +Here's a list of what you can't do with subgroups: + +- [GitLab Pages](../../project/pages/index.md) are not currently working for + projects hosted under a subgroup. That means that only projects hosted under + the first parent group will work. +- Group level labels don't work in subgroups / sub projects +- It is not possible to share a project with a group that's an ancestor of + the group the project is in. That means you can only share as you walk down + the hierarchy. For example, `group/subgroup01/project` **cannot** be shared + with `group`, but can be shared with `group/subgroup02` or + `group/subgroup01/subgroup03`. + +[ce-2772]: https://gitlab.com/gitlab-org/gitlab-ce/issues/2772 +[permissions]: ../../permissions.md#group +[reserved]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/validators/namespace_validator.rb diff --git a/doc/user/permissions.md b/doc/user/permissions.md index b49a244160a..0ea6d01411f 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -81,6 +81,7 @@ group. |-------------------------|-------|----------|-----------|--------|-------| | Browse group | ✓ | ✓ | ✓ | ✓ | ✓ | | Edit group | | | | | ✓ | +| Create subgroup | | | | | ✓ | | Create project in group | | | | ✓ | ✓ | | Manage group members | | | | | ✓ | | Remove group | | | | | ✓ | diff --git a/doc/workflow/README.md b/doc/workflow/README.md index 9e7ee47387c..a286a23765d 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -40,3 +40,4 @@ - [Importing from SVN, GitHub, Bitbucket, etc](importing/README.md) - [Todos](todos.md) - [Snippets](../user/snippets.md) +- [Nested groups](../user/group/subgroups/index.md) -- cgit v1.2.1 From 1913f1ed9efced37cc597515ea4c7219eb17b4be Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Fri, 10 Mar 2017 14:15:28 +0100 Subject: Add info on group membership [ci skip] --- doc/user/group/subgroups/index.md | 39 +++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md index 9522e6fc4ba..2338d8e9b42 100644 --- a/doc/user/group/subgroups/index.md +++ b/doc/user/group/subgroups/index.md @@ -104,6 +104,9 @@ When you add a member to a subgroup, they inherit the membership and permission level from the parent group. This model allows access to nested groups if you have membership in one of its parents. +The group permissions for a member can be changed only by Owners and only on +the **Members** page of the group the member was added. + You can tell if a member has inherited the permissions from a parent group by looking at the group's **Members** page. @@ -111,19 +114,35 @@ looking at the group's **Members** page. From the image above, we can deduct the following things: -- There are 5 members that have access to the group **four** -- Administrator is the Owner and member of all subgroups -- User0 is a Reporter and has inherited their permissions from group **one** - which is above the hierarchy of group **four** +- There are 5 members that have access to the group `four` +- User0 is a Reporter and has inherited their permissions from group `one` + which is above the hierarchy of group `four` - User1 is a Developer and has inherited their permissions from group - **one/two** which is above the hierarchy of group **four** + `one/two` which is above the hierarchy of group `four` - User2 is a Developer and has inherited their permissions from group - **one/two/three** which is above the hierarchy of group **four** -- User3 is a Master of group **four**, there is no indication of a parent - group therefore they belong to group **four** + `one/two/three` which is above the hierarchy of group `four` +- For User3 there is no indication of a parent group, therefore they belong to + group `four`, the one we're inspecting +- Administrator is the Owner and member of **all** subgroups and for that reason, + same as User3, there is no indication of an ancestor group -The group permissions for a member can be changed only by Owners and only on -the **Members** page of the group the member was added. +### Overriding the ancestor group membership + +>**Note:** +You need to be an Owner of a group in order to be able to add members to it. + +To override the membership of an ancestor group, simply add the user in the new +subgroup again, but with different permissions. + +For example, if User0 was first added to group `one/two` with Developer +permissions, then they will inherit those permissions in every other subgroup +of `one/two`. To give them Master access to `one/two/three`, you would add them +again in that group as Master. Removing them from that group, the permissions +will fallback to those of the ancestor group. + +Note that the higher permission wins, so if in the above example the permissions +where reversed, User0 would have Master access to all groups, even to the one +that was explicitly given Developer access. ## Mentioning subgroups -- cgit v1.2.1 From f35d7a16595c199add9a582b8a80f8da75e5544d Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Fri, 10 Mar 2017 14:18:48 +0100 Subject: Fix wording [ci skip] --- doc/user/group/subgroups/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md index 2338d8e9b42..ff28f458f99 100644 --- a/doc/user/group/subgroups/index.md +++ b/doc/user/group/subgroups/index.md @@ -147,9 +147,9 @@ that was explicitly given Developer access. ## Mentioning subgroups Mentioning groups (`@group`) in issues, commits and merge requests, would -mention all members of that group. Now with subgroups, there is a more granular +notify all members of that group. Now with subgroups, there is a more granular support if you want to split your group's structure. Mentioning works as before -and you can choose the group of people to be summoned. +and you can choose the group of people to be notified. ![Mentioning subgroups](img/mention_subgroups.png) -- cgit v1.2.1 From b5142f92a03c1b2a58ee583e2d0632150e74d45a Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 14 Mar 2017 21:41:46 +0100 Subject: Address subgroups docs review [ci skip] --- doc/user/group/subgroups/index.md | 86 ++++++++++++++++++--------------------- 1 file changed, 39 insertions(+), 47 deletions(-) diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md index ff28f458f99..ce5da07c61a 100644 --- a/doc/user/group/subgroups/index.md +++ b/doc/user/group/subgroups/index.md @@ -2,7 +2,7 @@ > [Introduced][ce-2772] in GitLab 9.0. -With subgroups (also called nested groups or hierarchical groups) you can have +With subgroups (aka nested groups or hierarchical groups) you can have up to 20 levels of nested groups, which among other things can help you to: - **Separate internal / external organizations.** Since every group @@ -16,50 +16,45 @@ up to 20 levels of nested groups, which among other things can help you to: ## Overview A group can have many subgroups inside it, and at the same time a group can have -only 1 parent group. It resembles a directory behavior, like the one below: +only 1 parent group. It resembles a directory behavior or a nested items list: -``` -group0 -└── subgroup01a -└── subgroup01b - └── subgroup02 - └── subgroup03 -``` +- Group 1 + - Group 1.1 + - Group 1.2 + - Group 1.2.1 + - Group 1.2.2 + - Group 1.2.2.1 In a real world example, imagine maintaining a GNU/Linux distribution with the first group being the name of the distro and subsequent groups split like: -``` -Organization Group - GNU/Linux distro - └── Category Subgroup - Packages - └── project - Package01 - └── project - Package02 - └── Category Subgroup - Software - └── project - Core - └── project - CLI - └── project - Android app - └── project - iOS app - └── Category Subgroup - Infra tools - └── project - Ansible playbooks -``` +- Organization Group - GNU/Linux distro + - Category Subgroup - Packages + - (project) Package01 + - (project) Package02 + - Category Subgroup - Software + - (project) Core + - (project) CLI + - (project) Android app + - (project) iOS app + - Category Subgroup - Infra tools + - (project) Ansible playbooks Another example of GitLab as a company would be the following: -``` -Organization Group - GitLab - └── Category Subroup - Marketing - └── project - Design - └── project - General - └── Category Subgroup - Software - └── project - GitLab CE - └── project - GitLab EE - └── project - Omnibus GitLab - └── project - GitLab Runner - └── project - GitLab Pages daemon - └── Category Subgroup - Infra tools - └── project - Chef cookbooks - └── Category Subgroup - Executive team -``` +- Organization Group - GitLab + - Category Subroup - Marketing + - (project) Design + - (project) General + - Category Subgroup - Software + - (project) GitLab CE + - (project) GitLab EE + - (project) Omnibus GitLab + - (project) GitLab Runner + - (project) GitLab Pages daemon + - Category Subgroup - Infra tools + - (project) Chef cookbooks + - Category Subgroup - Executive team --- @@ -131,18 +126,15 @@ From the image above, we can deduct the following things: >**Note:** You need to be an Owner of a group in order to be able to add members to it. -To override the membership of an ancestor group, simply add the user in the new -subgroup again, but with different permissions. +To override a user's membership of an ancestor group (the first group they were +added to), simply add the user in the new subgroup again, but with different +permissions. -For example, if User0 was first added to group `one/two` with Developer +For example, if User0 was first added to group `group-1/group-1-1` with Developer permissions, then they will inherit those permissions in every other subgroup -of `one/two`. To give them Master access to `one/two/three`, you would add them -again in that group as Master. Removing them from that group, the permissions -will fallback to those of the ancestor group. - -Note that the higher permission wins, so if in the above example the permissions -where reversed, User0 would have Master access to all groups, even to the one -that was explicitly given Developer access. +of `group-1/group-1-1`. To give them Master access to `group-1/group-1-1/group1-1-1`, +you would add them again in that group as Master. Removing them from that group, +the permissions will fallback to those of the ancestor group. ## Mentioning subgroups -- cgit v1.2.1 From 6890327762eaeca572ada783804a9c7af01e6144 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 10 Mar 2017 15:34:29 -0600 Subject: Copy code as GFM from diffs, blobs and GFM code blocks --- app/assets/javascripts/copy_as_gfm.js | 72 ++- changelogs/unreleased/dm-copy-code-as-gfm.yml | 4 + lib/banzai/filter/syntax_highlight_filter.rb | 13 +- lib/gitlab/highlight.rb | 4 +- lib/rouge/formatters/html_gitlab.rb | 5 +- spec/features/copy_as_gfm_spec.rb | 782 +++++++++++++++----------- 6 files changed, 537 insertions(+), 343 deletions(-) create mode 100644 changelogs/unreleased/dm-copy-code-as-gfm.yml diff --git a/app/assets/javascripts/copy_as_gfm.js b/app/assets/javascripts/copy_as_gfm.js index 0fb7bde1fd6..67f7226fe82 100644 --- a/app/assets/javascripts/copy_as_gfm.js +++ b/app/assets/javascripts/copy_as_gfm.js @@ -118,10 +118,10 @@ const gfmRules = { }, SyntaxHighlightFilter: { 'pre.code.highlight'(el, t) { - const text = t.trim(); + const text = t.trimRight(); let lang = el.getAttribute('lang'); - if (lang === 'plaintext') { + if (!lang || lang === 'plaintext') { lang = ''; } @@ -157,7 +157,7 @@ const gfmRules = { const backticks = Array(backtickCount + 1).join('`'); const spaceOrNoSpace = backtickCount > 1 ? ' ' : ''; - return backticks + spaceOrNoSpace + text + spaceOrNoSpace + backticks; + return backticks + spaceOrNoSpace + text.trim() + spaceOrNoSpace + backticks; }, 'blockquote'(el, text) { return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n'); @@ -273,28 +273,29 @@ const gfmRules = { class CopyAsGFM { constructor() { - $(document).on('copy', '.md, .wiki', this.handleCopy); - $(document).on('paste', '.js-gfm-input', this.handlePaste); + $(document).on('copy', '.md, .wiki', (e) => { this.copyAsGFM(e, CopyAsGFM.transformGFMSelection); }); + $(document).on('copy', 'pre.code.highlight, .diff-content .line_content', (e) => { this.copyAsGFM(e, CopyAsGFM.transformCodeSelection); }); + $(document).on('paste', '.js-gfm-input', this.pasteGFM.bind(this)); } - handleCopy(e) { + copyAsGFM(e, transformer) { const clipboardData = e.originalEvent.clipboardData; if (!clipboardData) return; const documentFragment = window.gl.utils.getSelectedFragment(); if (!documentFragment) return; - // If the documentFragment contains more than just Markdown, don't copy as GFM. - if (documentFragment.querySelector('.md, .wiki')) return; + const el = transformer(documentFragment.cloneNode(true)); + if (!el) return; e.preventDefault(); - clipboardData.setData('text/plain', documentFragment.textContent); + e.stopPropagation(); - const gfm = CopyAsGFM.nodeToGFM(documentFragment); - clipboardData.setData('text/x-gfm', gfm); + clipboardData.setData('text/plain', el.textContent); + clipboardData.setData('text/x-gfm', CopyAsGFM.nodeToGFM(el)); } - handlePaste(e) { + pasteGFM(e) { const clipboardData = e.originalEvent.clipboardData; if (!clipboardData) return; @@ -306,7 +307,54 @@ class CopyAsGFM { window.gl.utils.insertText(e.target, gfm); } + static transformGFMSelection(documentFragment) { + // If the documentFragment contains more than just Markdown, don't copy as GFM. + if (documentFragment.querySelector('.md, .wiki')) return null; + + return documentFragment; + } + + static transformCodeSelection(documentFragment) { + const lineEls = documentFragment.querySelectorAll('.line'); + + let codeEl; + if (lineEls.length > 1) { + codeEl = document.createElement('pre'); + codeEl.className = 'code highlight'; + + const lang = lineEls[0].getAttribute('lang'); + if (lang) { + codeEl.setAttribute('lang', lang); + } + } else { + codeEl = document.createElement('code'); + } + + if (lineEls.length > 0) { + for (let i = 0; i < lineEls.length; i += 1) { + const lineEl = lineEls[i]; + codeEl.appendChild(lineEl); + codeEl.appendChild(document.createTextNode('\n')); + } + } else { + codeEl.appendChild(documentFragment); + } + + return codeEl; + } + + static selectionToGFM(documentFragment, transformer) { + const el = transformer(documentFragment.cloneNode(true)); + if (!el) return null; + + return CopyAsGFM.nodeToGFM(el); + } + static nodeToGFM(node) { + if (node.nodeType === Node.COMMENT_NODE) { + return ''; + } + if (node.nodeType === Node.TEXT_NODE) { return node.textContent; } diff --git a/changelogs/unreleased/dm-copy-code-as-gfm.yml b/changelogs/unreleased/dm-copy-code-as-gfm.yml new file mode 100644 index 00000000000..15ae2da44a3 --- /dev/null +++ b/changelogs/unreleased/dm-copy-code-as-gfm.yml @@ -0,0 +1,4 @@ +--- +title: Copy code as GFM from diffs, blobs and GFM code blocks +merge_request: +author: diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb index a447e2b8bff..9f09ca90697 100644 --- a/lib/banzai/filter/syntax_highlight_filter.rb +++ b/lib/banzai/filter/syntax_highlight_filter.rb @@ -5,8 +5,6 @@ module Banzai # HTML Filter to highlight fenced code blocks # class SyntaxHighlightFilter < HTML::Pipeline::Filter - include Rouge::Plugins::Redcarpet - def call doc.search('pre > code').each do |node| highlight_node(node) @@ -23,7 +21,7 @@ module Banzai lang = lexer.tag begin - code = format(lex(lexer, code)) + code = Rouge::Formatters::HTMLGitlab.format(lex(lexer, code), tag: lang) css_classes << " js-syntax-highlight #{lang}" rescue @@ -45,10 +43,6 @@ module Banzai lexer.lex(code) end - def format(tokens) - rouge_formatter.format(tokens) - end - def lexer_for(language) (Rouge::Lexer.find(language) || Rouge::Lexers::PlainText).new end @@ -57,11 +51,6 @@ module Banzai # Replace the parent `pre` element with the entire highlighted block node.parent.replace(highlighted) end - - # Override Rouge::Plugins::Redcarpet#rouge_formatter - def rouge_formatter(lexer = nil) - @rouge_formatter ||= Rouge::Formatters::HTML.new - end end end end diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb index 9360afedfcb..d787d5db4a0 100644 --- a/lib/gitlab/highlight.rb +++ b/lib/gitlab/highlight.rb @@ -14,7 +14,7 @@ module Gitlab end def initialize(blob_name, blob_content, repository: nil) - @formatter = Rouge::Formatters::HTMLGitlab.new + @formatter = Rouge::Formatters::HTMLGitlab @repository = repository @blob_name = blob_name @blob_content = blob_content @@ -28,7 +28,7 @@ module Gitlab hl_lexer = self.lexer end - @formatter.format(hl_lexer.lex(text, continue: continue)).html_safe + @formatter.format(hl_lexer.lex(text, continue: continue), tag: hl_lexer.tag).html_safe rescue @formatter.format(Rouge::Lexers::PlainText.lex(text)).html_safe end diff --git a/lib/rouge/formatters/html_gitlab.rb b/lib/rouge/formatters/html_gitlab.rb index 4edfd015074..ec95ddf03ea 100644 --- a/lib/rouge/formatters/html_gitlab.rb +++ b/lib/rouge/formatters/html_gitlab.rb @@ -6,9 +6,10 @@ module Rouge # Creates a new Rouge::Formatter::HTMLGitlab instance. # # [+linenostart+] The line number for the first line (default: 1). - def initialize(linenostart: 1) + def initialize(linenostart: 1, tag: nil) @linenostart = linenostart @line_number = linenostart + @tag = tag end def stream(tokens, &b) @@ -17,7 +18,7 @@ module Rouge yield "\n" unless is_first is_first = false - yield %() + yield %() line.each { |token, value| yield span(token, value.chomp) } yield %() diff --git a/spec/features/copy_as_gfm_spec.rb b/spec/features/copy_as_gfm_spec.rb index 4638812b2d9..f134d4be154 100644 --- a/spec/features/copy_as_gfm_spec.rb +++ b/spec/features/copy_as_gfm_spec.rb @@ -2,437 +2,589 @@ require 'spec_helper' describe 'Copy as GFM', feature: true, js: true do include GitlabMarkdownHelper + include RepoHelpers include ActionView::Helpers::JavaScriptHelper before do - @feat = MarkdownFeature.new + login_as :admin + end - # `markdown` helper expects a `@project` variable - @project = @feat.project + describe 'Copying rendered GFM' do + before do + @feat = MarkdownFeature.new - visit namespace_project_issue_path(@project.namespace, @project, @feat.issue) - end + # `markdown` helper expects a `@project` variable + @project = @feat.project - # The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert GitLab Flavored Markdown (GFM) to HTML. - # The handlers defined in app/assets/javascripts/copy_as_gfm.js.es6 consequently convert that same HTML to GFM. - # To make sure these filters and handlers are properly aligned, this spec tests the GFM-to-HTML-to-GFM cycle - # by verifying (`html_to_gfm(gfm_to_html(gfm)) == gfm`) for a number of examples of GFM for every filter, using the `verify` helper. + visit namespace_project_issue_path(@project.namespace, @project, @feat.issue) + end - # These are all in a single `it` for performance reasons. - it 'works', :aggregate_failures do - verify( - 'nesting', + # The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert GitLab Flavored Markdown (GFM) to HTML. + # The handlers defined in app/assets/javascripts/copy_as_gfm.js.es6 consequently convert that same HTML to GFM. + # To make sure these filters and handlers are properly aligned, this spec tests the GFM-to-HTML-to-GFM cycle + # by verifying (`html_to_gfm(gfm_to_html(gfm)) == gfm`) for a number of examples of GFM for every filter, using the `verify` helper. - '> 1. [x] **[$`2 + 2`$ {-=-}{+=+} 2^2 ~~:thumbsup:~~](http://google.com)**' - ) + # These are all in a single `it` for performance reasons. + it 'works', :aggregate_failures do + verify( + 'nesting', - verify( - 'a real world example from the gitlab-ce README', + '> 1. [x] **[$`2 + 2`$ {-=-}{+=+} 2^2 ~~:thumbsup:~~](http://google.com)**' + ) - <<-GFM.strip_heredoc - # GitLab + verify( + 'a real world example from the gitlab-ce README', - [![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master) - [![CE coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby) - [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq) - [![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42) + <<-GFM.strip_heredoc + # GitLab - ## Canonical source + [![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master) + [![CE coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby) + [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq) + [![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42) - The canonical source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/). + ## Canonical source - ## Open source software to collaborate on code + The canonical source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/). - To see how GitLab looks please see the [features page on our website](https://about.gitlab.com/features/). + ## Open source software to collaborate on code + To see how GitLab looks please see the [features page on our website](https://about.gitlab.com/features/). - - Manage Git repositories with fine grained access controls that keep your code secure - - Perform code reviews and enhance collaboration with merge requests + - Manage Git repositories with fine grained access controls that keep your code secure - - Complete continuous integration (CI) and CD pipelines to builds, test, and deploy your applications + - Perform code reviews and enhance collaboration with merge requests - - Each project can also have an issue tracker, issue board, and a wiki + - Complete continuous integration (CI) and CD pipelines to builds, test, and deploy your applications - - Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises + - Each project can also have an issue tracker, issue board, and a wiki - - Completely free and open source (MIT Expat license) - GFM - ) + - Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises - verify( - 'InlineDiffFilter', + - Completely free and open source (MIT Expat license) + GFM + ) - '{-Deleted text-}', - '{+Added text+}' - ) + verify( + 'InlineDiffFilter', - verify( - 'TaskListFilter', + '{-Deleted text-}', + '{+Added text+}' + ) - '- [ ] Unchecked task', - '- [x] Checked task', - '1. [ ] Unchecked numbered task', - '1. [x] Checked numbered task' - ) + verify( + 'TaskListFilter', - verify( - 'ReferenceFilter', + '- [ ] Unchecked task', + '- [x] Checked task', + '1. [ ] Unchecked numbered task', + '1. [x] Checked numbered task' + ) - # issue reference - @feat.issue.to_reference, - # full issue reference - @feat.issue.to_reference(full: true), - # issue URL - namespace_project_issue_url(@project.namespace, @project, @feat.issue), - # issue URL with note anchor - namespace_project_issue_url(@project.namespace, @project, @feat.issue, anchor: 'note_123'), - # issue link - "[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue)})", - # issue link with note anchor - "[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue, anchor: 'note_123')})", - ) + verify( + 'ReferenceFilter', - verify( - 'AutolinkFilter', + # issue reference + @feat.issue.to_reference, + # full issue reference + @feat.issue.to_reference(full: true), + # issue URL + namespace_project_issue_url(@project.namespace, @project, @feat.issue), + # issue URL with note anchor + namespace_project_issue_url(@project.namespace, @project, @feat.issue, anchor: 'note_123'), + # issue link + "[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue)})", + # issue link with note anchor + "[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue, anchor: 'note_123')})", + ) - 'https://example.com' - ) + verify( + 'AutolinkFilter', - verify( - 'TableOfContentsFilter', + 'https://example.com' + ) - '[[_TOC_]]' - ) + verify( + 'TableOfContentsFilter', - verify( - 'EmojiFilter', + '[[_TOC_]]' + ) - ':thumbsup:' - ) + verify( + 'EmojiFilter', - verify( - 'ImageLinkFilter', - - '![Image](https://example.com/image.png)' - ) + ':thumbsup:' + ) - verify( - 'VideoLinkFilter', + verify( + 'ImageLinkFilter', + + '![Image](https://example.com/image.png)' + ) - '![Video](https://example.com/video.mp4)' - ) + verify( + 'VideoLinkFilter', - verify( - 'MathFilter: math as converted from GFM to HTML', + '![Video](https://example.com/video.mp4)' + ) - '$`c = \pm\sqrt{a^2 + b^2}`$', + verify( + 'MathFilter: math as converted from GFM to HTML', - # math block - <<-GFM.strip_heredoc - ```math - c = \pm\sqrt{a^2 + b^2} - ``` - GFM - ) + '$`c = \pm\sqrt{a^2 + b^2}`$', - aggregate_failures('MathFilter: math as transformed from HTML to KaTeX') do - gfm = '$`c = \pm\sqrt{a^2 + b^2}`$' + # math block + <<-GFM.strip_heredoc + ```math + c = \pm\sqrt{a^2 + b^2} + ``` + GFM + ) - html = <<-HTML.strip_heredoc - - - - - - c - = - ± - - - - a - 2 - - + - - b - 2 - - - - - c = \\pm\\sqrt{a^2 + b^2} - - - -