diff --git a/.eslintrc.yml b/.eslintrc.yml index 2b881d5f201..2612fd3371d 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -5,6 +5,7 @@ globals: gl: false gon: false localStorage: false + IS_EE: false plugins: - import - html diff --git a/.gitlab/ci/docs.gitlab-ci.yml b/.gitlab/ci/docs.gitlab-ci.yml index 6acbce6cf0c..5bc109f2b7f 100644 --- a/.gitlab/ci/docs.gitlab-ci.yml +++ b/.gitlab/ci/docs.gitlab-ci.yml @@ -66,9 +66,7 @@ docs lint: - mv doc/ /tmp/gitlab-docs/content/$DOCS_GITLAB_REPO_SUFFIX - cd /tmp/gitlab-docs # Lint Markdown - # https://github.com/markdownlint/markdownlint/blob/master/docs/RULES.md - - bundle exec mdl content/$DOCS_GITLAB_REPO_SUFFIX/**/*.md --ignore-front-matter --rules \ - MD004,MD032,MD034 + - bundle exec mdl content/$DOCS_GITLAB_REPO_SUFFIX -c $CI_PROJECT_DIR/.mdlrc # Build HTML from Markdown - bundle exec nanoc # Check the internal links diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml index 01c96e06547..2d06a8acc58 100644 --- a/.gitlab/ci/rails.gitlab-ci.yml +++ b/.gitlab/ci/rails.gitlab-ci.yml @@ -139,7 +139,7 @@ setup-test-env: rspec unit pg: <<: *rspec-metadata-pg - parallel: 25 + parallel: 20 rspec integration pg: <<: *rspec-metadata-pg @@ -152,7 +152,7 @@ rspec system pg: rspec unit pg-10: <<: *rspec-metadata-pg-10 <<: *only-schedules-master - parallel: 25 + parallel: 20 rspec integration pg-10: <<: *rspec-metadata-pg-10 diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml index 61fd48fd72e..ce019de213b 100644 --- a/.gitlab/ci/review.gitlab-ci.yml +++ b/.gitlab/ci/review.gitlab-ci.yml @@ -77,7 +77,7 @@ schedule:review-build-cng: .review-deploy-base: &review-deploy-base <<: *review-base allow_failure: true - retry: 2 + retry: 1 stage: review variables: HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}" diff --git a/.mdlrc b/.mdlrc new file mode 100644 index 00000000000..b2127dadc22 --- /dev/null +++ b/.mdlrc @@ -0,0 +1,4 @@ +# See https://github.com/markdownlint/markdownlint/blob/master/docs/configuration.md + +ignore_front_matter true +style File.expand_path('.mdlrc.style', __dir__) diff --git a/.mdlrc.style b/.mdlrc.style new file mode 100644 index 00000000000..30abf03f462 --- /dev/null +++ b/.mdlrc.style @@ -0,0 +1,7 @@ +# See https://github.com/markdownlint/markdownlint/blob/master/docs/RULES.md + +rule 'MD001' +# False positives, see https://github.com/markdownlint/markdownlint/issues/261 +# rule 'MD004', style: :dash +rule 'MD032' +rule 'MD034' diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 698570efb07..41714eefa97 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -466,12 +466,10 @@ Rails/LinkToBlank: Rails/Presence: Exclude: - 'app/models/ci/pipeline.rb' - - 'app/models/clusters/platforms/kubernetes.rb' - 'app/models/concerns/mentionable.rb' - 'app/models/project_services/hipchat_service.rb' - 'app/models/project_services/irker_service.rb' - 'app/models/project_services/jira_service.rb' - - 'app/models/project_services/kubernetes_service.rb' - 'app/models/project_services/packagist_service.rb' - 'app/models/wiki_page.rb' - 'lib/gitlab/github_import/importer/releases_importer.rb' @@ -514,7 +512,6 @@ Security/YAMLLoad: - 'spec/config/mail_room_spec.rb' - 'spec/initializers/secret_token_spec.rb' - 'spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb' - - 'spec/models/project_services/kubernetes_service_spec.rb' # Offense count: 34 # Configuration parameters: EnforcedStyle. diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 7f3a46a841e..ba0a719118c 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -1.49.0 +1.51.0 diff --git a/Gemfile b/Gemfile index 1264d75eac6..f32e899342b 100644 --- a/Gemfile +++ b/Gemfile @@ -132,7 +132,7 @@ gem 'wikicloth', '0.8.1' gem 'asciidoctor', '~> 2.0.10' gem 'asciidoctor-include-ext', '~> 0.3.1', require: false gem 'asciidoctor-plantuml', '0.0.9' -gem 'rouge', '~> 3.1' +gem 'rouge', '~> 3.5' gem 'truncato', '~> 0.7.11' gem 'bootstrap_form', '~> 4.2.0' gem 'nokogiri', '~> 1.10.3' @@ -309,7 +309,7 @@ group :metrics do gem 'influxdb', '~> 0.2', require: false # Prometheus - gem 'prometheus-client-mmap', '~> 0.9.6' + gem 'prometheus-client-mmap', '~> 0.9.8' gem 'raindrops', '~> 0.18' end @@ -368,6 +368,7 @@ group :development, :test do gem 'haml_lint', '~> 0.31.0', require: false gem 'simplecov', '~> 0.16.1', require: false gem 'bundler-audit', '~> 0.5.0', require: false + gem 'mdl', '~> 0.5.0', require: false gem 'benchmark-ips', '~> 2.3.0', require: false @@ -419,7 +420,7 @@ gem 'vmstat', '~> 2.3.0' gem 'sys-filesystem', '~> 1.1.6' # SSH host key support -gem 'net-ssh', '~> 5.0' +gem 'net-ssh', '~> 5.2' gem 'sshkey', '~> 2.0' # Required for ED25519 SSH host key support @@ -429,7 +430,7 @@ group :ed25519 do end # Gitaly GRPC client -gem 'gitaly-proto', '~> 1.32.0', require: 'gitaly' +gem 'gitaly-proto', '~> 1.36.0', require: 'gitaly' gem 'grpc', '~> 1.19.0' diff --git a/Gemfile.lock b/Gemfile.lock index 5b648d43137..85b4c32f168 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -303,7 +303,7 @@ GEM gettext_i18n_rails (>= 0.7.1) po_to_json (>= 1.0.0) rails (>= 3.2.0) - gitaly-proto (1.32.0) + gitaly-proto (1.36.0) grpc (~> 1.0) github-markup (1.7.0) gitlab-labkit (0.3.0) @@ -459,6 +459,7 @@ GEM kgio (2.11.2) knapsack (1.17.0) rake + kramdown (1.17.0) kubeclient (4.2.2) http (~> 3.0) recursive-open-struct (~> 1.0, >= 1.0.4) @@ -492,6 +493,10 @@ GEM mail (2.7.1) mini_mime (>= 0.1.1) mail_room (0.9.1) + mdl (0.5.0) + kramdown (~> 1.12, >= 1.12.0) + mixlib-cli (~> 1.7, >= 1.7.0) + mixlib-config (~> 2.2, >= 2.2.1) memoist (0.16.0) memoizable (0.4.2) thread_safe (~> 0.3, >= 0.3.1) @@ -505,6 +510,9 @@ GEM mini_mime (1.0.1) mini_portile2 (2.4.0) minitest (5.11.3) + mixlib-cli (1.7.0) + mixlib-config (2.2.18) + tomlrb msgpack (1.2.10) multi_json (1.13.1) multi_xml (0.6.0) @@ -515,7 +523,7 @@ GEM mysql2 (0.4.10) nakayoshi_fork (0.0.4) net-ldap (0.16.0) - net-ssh (5.0.1) + net-ssh (5.2.0) netrc (0.11.0) nio4r (2.3.1) nokogiri (1.10.3) @@ -652,7 +660,7 @@ GEM parser unparser procto (0.0.3) - prometheus-client-mmap (0.9.6) + prometheus-client-mmap (0.9.8) pry (0.11.3) coderay (~> 1.1.0) method_source (~> 0.9.0) @@ -770,7 +778,7 @@ GEM retriable (3.1.2) rinku (2.0.0) rotp (2.1.2) - rouge (3.4.1) + rouge (3.5.1) rqrcode (0.7.0) chunky_png rqrcode-rails3 (0.1.7) @@ -943,6 +951,7 @@ GEM parslet (~> 1.8.0) toml-rb (1.0.0) citrus (~> 3.0, > 3.0) + tomlrb (1.2.8) truncato (0.7.11) htmlentities (~> 4.3.1) nokogiri (>= 1.7.0, <= 2.0) @@ -1092,7 +1101,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.3) - gitaly-proto (~> 1.32.0) + gitaly-proto (~> 1.36.0) github-markup (~> 1.7.0) gitlab-labkit (~> 0.3.0) gitlab-markup (~> 1.7.0) @@ -1134,6 +1143,7 @@ DEPENDENCIES lograge (~> 0.5) loofah (~> 2.2) mail_room (~> 0.9.1) + mdl (~> 0.5.0) memory_profiler (~> 0.9) method_source (~> 0.8) mimemagic (~> 0.3.2) @@ -1142,7 +1152,7 @@ DEPENDENCIES mysql2 (~> 0.4.10) nakayoshi_fork (~> 0.0.4) net-ldap - net-ssh (~> 5.0) + net-ssh (~> 5.2) nokogiri (~> 1.10.3) oauth2 (~> 1.4) octokit (~> 4.9) @@ -1173,7 +1183,7 @@ DEPENDENCIES peek-redis (~> 1.2.0) pg (~> 1.1) premailer-rails (~> 1.9.7) - prometheus-client-mmap (~> 0.9.6) + prometheus-client-mmap (~> 0.9.8) pry-byebug (~> 3.5.1) pry-rails (~> 0.3.4) puma (~> 3.12) @@ -1199,7 +1209,7 @@ DEPENDENCIES redis-rails (~> 5.0.2) request_store (~> 1.3) responders (~> 2.0) - rouge (~> 3.1) + rouge (~> 3.5) rqrcode-rails3 (~> 0.1.7) rspec-parameterized rspec-rails (~> 3.7.0) diff --git a/app/assets/images/auth_buttons/salesforce_64.png b/app/assets/images/auth_buttons/salesforce_64.png new file mode 100644 index 00000000000..c8a86a0c515 Binary files /dev/null and b/app/assets/images/auth_buttons/salesforce_64.png differ diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 4f66a5d080c..a649c521405 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -24,6 +24,7 @@ const Api = { issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key', projectTemplatePath: '/api/:version/projects/:id/templates/:type/:key', projectTemplatesPath: '/api/:version/projects/:id/templates/:type', + userCountsPath: '/api/:version/user_counts', usersPath: '/api/:version/users.json', userPath: '/api/:version/users/:id', userStatusPath: '/api/:version/users/:id/status', @@ -312,6 +313,11 @@ const Api = { }); }, + userCounts() { + const url = Api.buildUrl(this.userCountsPath); + return axios.get(url); + }, + userStatus(id, options) { const url = Api.buildUrl(this.userStatusPath).replace(':id', encodeURIComponent(id)); return axios.get(url, { diff --git a/app/assets/javascripts/boards/components/board_blank_state.vue b/app/assets/javascripts/boards/components/board_blank_state.vue index f58149c9f7b..d8b0b60c183 100644 --- a/app/assets/javascripts/boards/components/board_blank_state.vue +++ b/app/assets/javascripts/boards/components/board_blank_state.vue @@ -61,7 +61,7 @@ export default {

{{ - __('BoardBlankState|Add the following default lists to your Issue Board with one click:') + s__('BoardBlankState|Add the following default lists to your Issue Board with one click:') }}

{{ - __( + s__( 'BoardBlankState|Starting out with the default set of lists will get you right on the way to making the most of your board.', ) }} @@ -86,10 +86,10 @@ export default { type="button" @click.stop="addDefaultLists" > - {{ __('BoardBlankState|Add default lists') }} + {{ s__('BoardBlankState|Add default lists') }}

diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js index 6b54e8baefb..b1b4b1c5508 100644 --- a/app/assets/javascripts/boards/filtered_search_boards.js +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -2,7 +2,6 @@ import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable import FilteredSearchContainer from '../filtered_search/container'; import FilteredSearchManager from '../filtered_search/filtered_search_manager'; import boardsStore from './stores/boards_store'; -import { isEE } from '~/lib/utils/common_utils'; export default class FilteredSearchBoards extends FilteredSearchManager { constructor(store, updateUrl = false, cantEdit = []) { @@ -10,7 +9,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager { page: 'boards', isGroupDecendent: true, stateFiltersSelector: '.issues-state-filters', - isGroup: isEE(), + isGroup: IS_EE, filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys, }); diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index a020765f335..23b107abefa 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -1,6 +1,7 @@ import $ from 'jquery'; import Vue from 'vue'; +import mountMultipleBoardsSwitcher from 'ee_else_ce/boards/mount_multiple_boards_switcher'; import Flash from '~/flash'; import { __ } from '~/locale'; import './models/label'; @@ -20,7 +21,7 @@ import modalMixin from './mixins/modal_mixins'; import './filters/due_date_filters'; import Board from './components/board'; import BoardSidebar from './components/board_sidebar'; -import initNewListDropdown from './components/new_list_dropdown'; +import initNewListDropdown from 'ee_else_ce/boards/components/new_list_dropdown'; import BoardAddIssuesModal from './components/modal/index.vue'; import '~/vue_shared/vue_resource_interceptor'; import { @@ -78,13 +79,14 @@ export default () => { }, }, created() { - gl.boardService = new BoardService({ + boardsStore.setEndpoints({ boardsEndpoint: this.boardsEndpoint, recentBoardsEndpoint: this.recentBoardsEndpoint, listsEndpoint: this.listsEndpoint, bulkUpdatePath: this.bulkUpdatePath, boardId: this.boardId, }); + gl.boardService = new BoardService(); boardsStore.rootPath = this.boardsEndpoint; eventHub.$on('updateTokens', this.updateTokens); @@ -278,4 +280,6 @@ export default () => { `, }); } + + mountMultipleBoardsSwitcher(); }; diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js index f858b162c6b..9069b35db9a 100644 --- a/app/assets/javascripts/boards/models/issue.js +++ b/app/assets/javascripts/boards/models/issue.js @@ -5,7 +5,7 @@ import Vue from 'vue'; import './label'; -import { isEE, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import IssueProject from './project'; import boardsStore from '../stores/boards_store'; @@ -91,13 +91,13 @@ class ListIssue { addMilestone(milestone) { const miletoneId = this.milestone ? this.milestone.id : null; - if (isEE && milestone.id !== miletoneId) { + if (IS_EE && milestone.id !== miletoneId) { this.milestone = new ListMilestone(milestone); } } removeMilestone(removeMilestone) { - if (isEE && removeMilestone && removeMilestone.id === this.milestone.id) { + if (IS_EE && removeMilestone && removeMilestone.id === this.milestone.id) { this.milestone = {}; } } diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index cd553d0c4af..7e0ccb9bd2a 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -4,7 +4,7 @@ import { __ } from '~/locale'; import ListLabel from './label'; import ListAssignee from './assignee'; -import { isEE, urlParamsToObject } from '~/lib/utils/common_utils'; +import { urlParamsToObject } from '~/lib/utils/common_utils'; import boardsStore from '../stores/boards_store'; import ListMilestone from './milestone'; @@ -58,7 +58,7 @@ class List { } else if (obj.user) { this.assignee = new ListAssignee(obj.user); this.title = this.assignee.name; - } else if (isEE && obj.milestone) { + } else if (IS_EE && obj.milestone) { this.milestone = new ListMilestone(obj.milestone); this.title = this.milestone.title; } @@ -85,7 +85,7 @@ class List { entityType = 'label_id'; } else if (this.assignee) { entityType = 'assignee_id'; - } else if (isEE && this.milestone) { + } else if (IS_EE && this.milestone) { entityType = 'milestone_id'; } @@ -205,7 +205,7 @@ class List { issue.addAssignee(this.assignee); } - if (isEE && this.milestone) { + if (IS_EE && this.milestone) { if (listFrom && listFrom.type === 'milestone') { issue.removeMilestone(listFrom.milestone); } diff --git a/app/assets/javascripts/boards/models/milestone.js b/app/assets/javascripts/boards/models/milestone.js index 6f81d6bc6f8..7201b6e91f5 100644 --- a/app/assets/javascripts/boards/models/milestone.js +++ b/app/assets/javascripts/boards/models/milestone.js @@ -1,11 +1,9 @@ -import { isEE } from '~/lib/utils/common_utils'; - export default class ListMilestone { constructor(obj) { this.id = obj.id; this.title = obj.title; - if (isEE) { + if (IS_EE) { this.path = obj.path; this.state = obj.state; this.webUrl = obj.web_url || obj.webUrl; diff --git a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js new file mode 100644 index 00000000000..bdb14a7f2f2 --- /dev/null +++ b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js @@ -0,0 +1,2 @@ +// this will be moved from EE to CE as part of https://gitlab.com/gitlab-org/gitlab-ce/issues/53811 +export default () => {}; diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js index 7d463f17ab1..580d04a3649 100644 --- a/app/assets/javascripts/boards/services/board_service.js +++ b/app/assets/javascripts/boards/services/board_service.js @@ -1,106 +1,66 @@ -import axios from '../../lib/utils/axios_utils'; -import { mergeUrlParams } from '../../lib/utils/url_utility'; +/* eslint-disable class-methods-use-this */ + +import boardsStore from '~/boards/stores/boards_store'; export default class BoardService { - constructor({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId, recentBoardsEndpoint }) { - this.boardsEndpoint = boardsEndpoint; - this.boardId = boardId; - this.listsEndpoint = listsEndpoint; - this.listsEndpointGenerate = `${listsEndpoint}/generate.json`; - this.bulkUpdatePath = bulkUpdatePath; - this.recentBoardsEndpoint = `${recentBoardsEndpoint}.json`; - } - generateBoardsPath(id) { - return `${this.boardsEndpoint}${id ? `/${id}` : ''}.json`; + return boardsStore.generateBoardsPath(id); } generateIssuesPath(id) { - return `${this.listsEndpoint}${id ? `/${id}` : ''}/issues`; + return boardsStore.generateIssuesPath(id); } static generateIssuePath(boardId, id) { - return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${ - id ? `/${id}` : '' - }`; + return boardsStore.generateIssuePath(boardId, id); } all() { - return axios.get(this.listsEndpoint); + return boardsStore.all(); } generateDefaultLists() { - return axios.post(this.listsEndpointGenerate, {}); + return boardsStore.generateDefaultLists(); } createList(entityId, entityType) { - const list = { - [entityType]: entityId, - }; - - return axios.post(this.listsEndpoint, { - list, - }); + return boardsStore.createList(entityId, entityType); } updateList(id, position) { - return axios.put(`${this.listsEndpoint}/${id}`, { - list: { - position, - }, - }); + return boardsStore.updateList(id, position); } destroyList(id) { - return axios.delete(`${this.listsEndpoint}/${id}`); + return boardsStore.destroyList(id); } getIssuesForList(id, filter = {}) { - const data = { id }; - Object.keys(filter).forEach(key => { - data[key] = filter[key]; - }); - - return axios.get(mergeUrlParams(data, this.generateIssuesPath(id))); + return boardsStore.getIssuesForList(id, filter); } moveIssue(id, fromListId = null, toListId = null, moveBeforeId = null, moveAfterId = null) { - return axios.put(BoardService.generateIssuePath(this.boardId, id), { - from_list_id: fromListId, - to_list_id: toListId, - move_before_id: moveBeforeId, - move_after_id: moveAfterId, - }); + return boardsStore.moveIssue(id, fromListId, toListId, moveBeforeId, moveAfterId); } newIssue(id, issue) { - return axios.post(this.generateIssuesPath(id), { - issue, - }); + return boardsStore.newIssue(id, issue); } getBacklog(data) { - return axios.get( - mergeUrlParams(data, `${gon.relative_url_root}/-/boards/${this.boardId}/issues.json`), - ); + return boardsStore.getBacklog(data); } bulkUpdate(issueIds, extraData = {}) { - const data = { - update: Object.assign(extraData, { - issuable_ids: issueIds.join(','), - }), - }; - - return axios.post(this.bulkUpdatePath, data); + return boardsStore.bulkUpdate(issueIds, extraData); } static getIssueInfo(endpoint) { - return axios.get(endpoint); + return boardsStore.getIssueInfo(endpoint); } static toggleIssueSubscription(endpoint) { - return axios.post(endpoint); + return boardsStore.toggleIssueSubscription(endpoint); } } diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index 4ba4cde6bae..b9cd4a143ef 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -8,6 +8,8 @@ import Cookies from 'js-cookie'; import BoardsStoreEE from 'ee_else_ce/boards/stores/boards_store_ee'; import { getUrlParamsArray, parseBoolean } from '~/lib/utils/common_utils'; import { __ } from '~/locale'; +import axios from '~/lib/utils/axios_utils'; +import { mergeUrlParams } from '~/lib/utils/url_utility'; import eventHub from '../eventhub'; const boardsStore = { @@ -28,6 +30,7 @@ const boardsStore = { }, currentPage: '', reload: false, + endpoints: {}, }, detail: { issue: {}, @@ -36,6 +39,19 @@ const boardsStore = { issue: {}, list: {}, }, + + setEndpoints({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId, recentBoardsEndpoint }) { + const listsEndpointGenerate = `${listsEndpoint}/generate.json`; + this.state.endpoints = { + boardsEndpoint, + boardId, + listsEndpoint, + listsEndpointGenerate, + bulkUpdatePath, + recentBoardsEndpoint: `${recentBoardsEndpoint}.json`, + }; + }, + create() { this.state.lists = []; this.filter.path = getUrlParamsArray().join('&'); @@ -229,6 +245,101 @@ const boardsStore = { setTimeTrackingLimitToHours(limitToHours) { this.timeTracking.limitToHours = parseBoolean(limitToHours); }, + + generateBoardsPath(id) { + return `${this.state.endpoints.boardsEndpoint}${id ? `/${id}` : ''}.json`; + }, + + generateIssuesPath(id) { + return `${this.state.endpoints.listsEndpoint}${id ? `/${id}` : ''}/issues`; + }, + + generateIssuePath(boardId, id) { + return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${ + id ? `/${id}` : '' + }`; + }, + + all() { + return axios.get(this.state.endpoints.listsEndpoint); + }, + + generateDefaultLists() { + return axios.post(this.state.endpoints.listsEndpointGenerate, {}); + }, + + createList(entityId, entityType) { + const list = { + [entityType]: entityId, + }; + + return axios.post(this.state.endpoints.listsEndpoint, { + list, + }); + }, + + updateList(id, position) { + return axios.put(`${this.state.endpoints.listsEndpoint}/${id}`, { + list: { + position, + }, + }); + }, + + destroyList(id) { + return axios.delete(`${this.state.endpoints.listsEndpoint}/${id}`); + }, + + getIssuesForList(id, filter = {}) { + const data = { id }; + Object.keys(filter).forEach(key => { + data[key] = filter[key]; + }); + + return axios.get(mergeUrlParams(data, this.generateIssuesPath(id))); + }, + + moveIssue(id, fromListId = null, toListId = null, moveBeforeId = null, moveAfterId = null) { + return axios.put(this.generateIssuePath(this.state.endpoints.boardId, id), { + from_list_id: fromListId, + to_list_id: toListId, + move_before_id: moveBeforeId, + move_after_id: moveAfterId, + }); + }, + + newIssue(id, issue) { + return axios.post(this.generateIssuesPath(id), { + issue, + }); + }, + + getBacklog(data) { + return axios.get( + mergeUrlParams( + data, + `${gon.relative_url_root}/-/boards/${this.state.endpoints.boardId}/issues.json`, + ), + ); + }, + + bulkUpdate(issueIds, extraData = {}) { + const data = { + update: Object.assign(extraData, { + issuable_ids: issueIds.join(','), + }), + }; + + return axios.post(this.state.endpoints.bulkUpdatePath, data); + }, + + getIssueInfo(endpoint) { + return axios.get(endpoint); + }, + + toggleIssueSubscription(endpoint) { + return axios.post(endpoint); + }, }; BoardsStoreEE.initEESpecific(boardsStore); diff --git a/app/assets/javascripts/branches/divergence_graph.js b/app/assets/javascripts/branches/divergence_graph.js index 96bc6a5f8e8..7dbaf984acf 100644 --- a/app/assets/javascripts/branches/divergence_graph.js +++ b/app/assets/javascripts/branches/divergence_graph.js @@ -36,7 +36,9 @@ export default endpoint => { }, 100); Object.entries(data).forEach(([branchName, val]) => { - const el = document.querySelector(`.js-branch-${branchName} .js-branch-divergence-graph`); + const el = document.querySelector( + `[data-name="${branchName}"] .js-branch-divergence-graph`, + ); if (!el) return; diff --git a/app/assets/javascripts/commons/index.js b/app/assets/javascripts/commons/index.js index 0d2fe2925d8..ad0f6cc1496 100644 --- a/app/assets/javascripts/commons/index.js +++ b/app/assets/javascripts/commons/index.js @@ -4,3 +4,6 @@ import './jquery'; import './bootstrap'; import './vue'; import '../lib/utils/axios_utils'; +import { openUserCountsBroadcast } from './nav/user_merge_requests'; + +openUserCountsBroadcast(); diff --git a/app/assets/javascripts/commons/nav/user_merge_requests.js b/app/assets/javascripts/commons/nav/user_merge_requests.js new file mode 100644 index 00000000000..8e694cca6a1 --- /dev/null +++ b/app/assets/javascripts/commons/nav/user_merge_requests.js @@ -0,0 +1,67 @@ +import Api from '~/api'; + +let channel; + +function broadcastCount(newCount) { + if (!channel) { + return; + } + + channel.postMessage(newCount); +} + +function updateUserMergeRequestCounts(newCount) { + const mergeRequestsCountEl = document.querySelector('.merge-requests-count'); + mergeRequestsCountEl.textContent = newCount.toLocaleString(); + mergeRequestsCountEl.classList.toggle('hidden', Number(newCount) === 0); +} + +/** + * Refresh user counts (and broadcast if open) + */ +export function refreshUserMergeRequestCounts() { + return Api.userCounts() + .then(({ data }) => { + const count = data.merge_requests; + + updateUserMergeRequestCounts(count); + broadcastCount(count); + }) + .catch(ex => { + console.error(ex); // eslint-disable-line no-console + }); +} + +/** + * Close the broadcast channel for user counts + */ +export function closeUserCountsBroadcast() { + if (!channel) { + return; + } + + channel.close(); + channel = null; +} + +/** + * Open the broadcast channel for user counts, adds user id so we only update + * + * **Please note:** + * Not supported in all browsers, but not polyfilling for now + * to keep bundle size small and + * no special functionality lost except cross tab notifications + */ +export function openUserCountsBroadcast() { + closeUserCountsBroadcast(); + + if (window.BroadcastChannel) { + const currentUserId = typeof gon !== 'undefined' && gon && gon.current_user_id; + if (currentUserId) { + channel = new BroadcastChannel(`mr_count_channel_${currentUserId}`); + channel.onmessage = ev => { + updateUserMergeRequestCounts(ev.data); + }; + } + } +} diff --git a/app/assets/javascripts/confidential_merge_request/components/dropdown.vue b/app/assets/javascripts/confidential_merge_request/components/dropdown.vue new file mode 100644 index 00000000000..444640980af --- /dev/null +++ b/app/assets/javascripts/confidential_merge_request/components/dropdown.vue @@ -0,0 +1,58 @@ + + + diff --git a/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue new file mode 100644 index 00000000000..99d77a75c23 --- /dev/null +++ b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue @@ -0,0 +1,132 @@ + + + diff --git a/app/assets/javascripts/confidential_merge_request/index.js b/app/assets/javascripts/confidential_merge_request/index.js new file mode 100644 index 00000000000..9672821d30e --- /dev/null +++ b/app/assets/javascripts/confidential_merge_request/index.js @@ -0,0 +1,30 @@ +import Vue from 'vue'; +import { parseBoolean } from '../lib/utils/common_utils'; +import ProjectFormGroup from './components/project_form_group.vue'; +import state from './state'; + +export function isConfidentialIssue() { + return parseBoolean(document.querySelector('.js-create-mr').dataset.isConfidential); +} + +export function canCreateConfidentialMergeRequest() { + return isConfidentialIssue() && Object.keys(state.selectedProject).length > 0; +} + +export function init() { + const el = document.getElementById('js-forked-project'); + + return new Vue({ + el, + render(h) { + return h(ProjectFormGroup, { + props: { + namespacePath: el.dataset.namespacePath, + projectPath: el.dataset.projectPath, + newForkPath: el.dataset.newForkPath, + helpPagePath: el.dataset.helpPagePath, + }, + }); + }, + }); +} diff --git a/app/assets/javascripts/confidential_merge_request/state.js b/app/assets/javascripts/confidential_merge_request/state.js new file mode 100644 index 00000000000..95b0580f4b9 --- /dev/null +++ b/app/assets/javascripts/confidential_merge_request/state.js @@ -0,0 +1,5 @@ +import Vue from 'vue'; + +export default Vue.observable({ + selectedProject: {}, +}); diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js index 8f5cece0788..052168bb21c 100644 --- a/app/assets/javascripts/create_merge_request_dropdown.js +++ b/app/assets/javascripts/create_merge_request_dropdown.js @@ -5,6 +5,12 @@ import Flash from './flash'; import DropLab from './droplab/drop_lab'; import ISetter from './droplab/plugins/input_setter'; import { __, sprintf } from './locale'; +import { + init as initConfidentialMergeRequest, + isConfidentialIssue, + canCreateConfidentialMergeRequest, +} from './confidential_merge_request'; +import confidentialMergeRequestState from './confidential_merge_request/state'; // Todo: Remove this when fixing issue in input_setter plugin const InputSetter = Object.assign({}, ISetter); @@ -12,6 +18,17 @@ const InputSetter = Object.assign({}, ISetter); const CREATE_MERGE_REQUEST = 'create-mr'; const CREATE_BRANCH = 'create-branch'; +function createEndpoint(projectPath, endpoint) { + if (canCreateConfidentialMergeRequest()) { + return endpoint.replace( + projectPath, + confidentialMergeRequestState.selectedProject.pathWithNamespace, + ); + } + + return endpoint; +} + export default class CreateMergeRequestDropdown { constructor(wrapperEl) { this.wrapperEl = wrapperEl; @@ -42,6 +59,8 @@ export default class CreateMergeRequestDropdown { this.refIsValid = true; this.refsPath = this.wrapperEl.dataset.refsPath; this.suggestedRef = this.refInput.value; + this.projectPath = this.wrapperEl.dataset.projectPath; + this.projectId = this.wrapperEl.dataset.projectId; // These regexps are used to replace // a backend generated new branch name and its source (ref) @@ -58,6 +77,14 @@ export default class CreateMergeRequestDropdown { }; this.init(); + + if (isConfidentialIssue()) { + this.createMergeRequestButton.setAttribute( + 'data-dropdown-trigger', + '#create-merge-request-dropdown', + ); + initConfidentialMergeRequest(); + } } available() { @@ -113,7 +140,9 @@ export default class CreateMergeRequestDropdown { this.isCreatingBranch = true; return axios - .post(this.createBranchPath) + .post(createEndpoint(this.projectPath, this.createBranchPath), { + confidential_issue_project_id: canCreateConfidentialMergeRequest() ? this.projectId : null, + }) .then(({ data }) => { this.branchCreated = true; window.location.href = data.url; @@ -125,7 +154,11 @@ export default class CreateMergeRequestDropdown { this.isCreatingMergeRequest = true; return axios - .post(this.createMrPath) + .post(this.createMrPath, { + target_project_id: canCreateConfidentialMergeRequest() + ? confidentialMergeRequestState.selectedProject.id + : null, + }) .then(({ data }) => { this.mergeRequestCreated = true; window.location.href = data.url; @@ -149,6 +182,8 @@ export default class CreateMergeRequestDropdown { } enable() { + if (!canCreateConfidentialMergeRequest()) return; + this.createMergeRequestButton.classList.remove('disabled'); this.createMergeRequestButton.removeAttribute('disabled'); @@ -205,7 +240,7 @@ export default class CreateMergeRequestDropdown { if (!ref) return false; return axios - .get(`${this.refsPath}${encodeURIComponent(ref)}`) + .get(`${createEndpoint(this.projectPath, this.refsPath)}${encodeURIComponent(ref)}`) .then(({ data }) => { const branches = data[Object.keys(data)[0]]; const tags = data[Object.keys(data)[1]]; @@ -325,6 +360,12 @@ export default class CreateMergeRequestDropdown { let xhr = null; event.preventDefault(); + if (isConfidentialIssue() && !event.target.classList.contains('js-create-target')) { + this.droplab.hooks.forEach(hook => hook.list.toggle()); + + return; + } + if (this.isBusy()) { return; } diff --git a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue index ca3285e9afd..a06dbd70ac5 100644 --- a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue +++ b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue @@ -24,6 +24,11 @@ export default { required: false, default: '', }, + hasDraft: { + type: Boolean, + required: false, + default: false, + }, }, computed: { className() { @@ -55,6 +60,7 @@ export default { :help-page-path="helpPagePath" /> () => false, + hasParallelDraftRight: () => () => false, }, }; diff --git a/app/assets/javascripts/event_tracking/notes.js b/app/assets/javascripts/event_tracking/notes.js index 2d1ec238274..1f70290c397 100644 --- a/app/assets/javascripts/event_tracking/notes.js +++ b/app/assets/javascripts/event_tracking/notes.js @@ -1 +1,2 @@ +// Noop function which has a EE counter-part export default () => {}; diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index 5fcb11a232e..03756a634d5 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -144,7 +144,9 @@ export default { 'triggerFilesChange', ]), initEditor() { - if (this.shouldHideEditor) return; + if (this.shouldHideEditor && (this.file.content || this.file.raw)) { + return; + } this.editor.clearEditor(); diff --git a/app/assets/javascripts/ide/lib/files.js b/app/assets/javascripts/ide/lib/files.js index b8abaa41f23..51278640b5b 100644 --- a/app/assets/javascripts/ide/lib/files.js +++ b/app/assets/javascripts/ide/lib/files.js @@ -77,6 +77,7 @@ export const decorateFiles = ({ const fileFolder = parent && insertParent(parent); if (name) { + const previewMode = viewerInformationForPath(name); parentPath = fileFolder && fileFolder.path; file = decorateData({ @@ -92,9 +93,9 @@ export const decorateFiles = ({ changed: tempFile, content, base64, - binary, + binary: (previewMode && previewMode.binary) || binary, rawPath, - previewMode: viewerInformationForPath(name), + previewMode, parentPath, }); diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js index c88244492e0..a52f1e235ed 100644 --- a/app/assets/javascripts/ide/stores/mutations/file.js +++ b/app/assets/javascripts/ide/stores/mutations/file.js @@ -1,6 +1,7 @@ import * as types from '../mutation_types'; import { sortTree } from '../utils'; import { diffModes } from '../../constants'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; export default { [types.SET_FILE_ACTIVE](state, { path, active }) { @@ -35,19 +36,18 @@ export default { } }, [types.SET_FILE_DATA](state, { data, file }) { - Object.assign(state.entries[file.path], { - id: data.id, - blamePath: data.blame_path, - commitsPath: data.commits_path, - permalink: data.permalink, - rawPath: data.raw_path, - binary: data.binary, - renderError: data.render_error, - raw: (state.entries[file.path] && state.entries[file.path].raw) || null, - baseRaw: null, - html: data.html, - size: data.size, - lastCommitSha: data.last_commit_sha, + const stateEntry = state.entries[file.path]; + const stagedFile = state.stagedFiles.find(f => f.path === file.path); + const openFile = state.openFiles.find(f => f.path === file.path); + const changedFile = state.changedFiles.find(f => f.path === file.path); + + [stateEntry, stagedFile, openFile, changedFile].forEach(f => { + if (f) { + Object.assign(f, convertObjectPropsToCamelCase(data, { dropKeys: ['raw', 'baseRaw'] }), { + raw: (stateEntry && stateEntry.raw) || null, + baseRaw: null, + }); + } }); }, [types.SET_FILE_RAW_DATA](state, { file, raw }) { diff --git a/app/assets/javascripts/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable_bulk_update_actions.js index bc9d7fcf30d..c855f3973b0 100644 --- a/app/assets/javascripts/issuable_bulk_update_actions.js +++ b/app/assets/javascripts/issuable_bulk_update_actions.js @@ -1,4 +1,4 @@ -/* eslint-disable consistent-return, func-names, array-callback-return, prefer-arrow-callback, no-unused-vars */ +/* eslint-disable consistent-return, func-names, array-callback-return, prefer-arrow-callback */ import $ from 'jquery'; import _ from 'underscore'; @@ -7,7 +7,7 @@ import Flash from './flash'; import { __ } from './locale'; export default { - init({ container, form, issues, prefixId } = {}) { + init({ form, issues, prefixId } = {}) { this.prefixId = prefixId || 'issue_'; this.form = form || this.getElement('.bulk-update'); this.$labelDropdown = this.form.find('.js-label-select'); diff --git a/app/assets/javascripts/issuable_index.js b/app/assets/javascripts/issuable_index.js index 16f88cddce3..f3f8b6ec715 100644 --- a/app/assets/javascripts/issuable_index.js +++ b/app/assets/javascripts/issuable_index.js @@ -2,26 +2,13 @@ import $ from 'jquery'; import axios from './lib/utils/axios_utils'; import flash from './flash'; import { s__, __ } from './locale'; -import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar'; -import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; +import issuableInitBulkUpdateSidebar from './issuable_init_bulk_update_sidebar'; export default class IssuableIndex { constructor(pagePrefix) { - this.initBulkUpdate(pagePrefix); + issuableInitBulkUpdateSidebar.init(pagePrefix); IssuableIndex.resetIncomingEmailToken(); } - initBulkUpdate(pagePrefix) { - const userCanBulkUpdate = $('.issues-bulk-update').length > 0; - const alreadyInitialized = Boolean(this.bulkUpdateSidebar); - - if (userCanBulkUpdate && !alreadyInitialized) { - IssuableBulkUpdateActions.init({ - prefixId: pagePrefix, - }); - - this.bulkUpdateSidebar = new IssuableBulkUpdateSidebar(); - } - } static resetIncomingEmailToken() { const $resetToken = $('.incoming-email-token-reset'); diff --git a/app/assets/javascripts/issuable_init_bulk_update_sidebar.js b/app/assets/javascripts/issuable_init_bulk_update_sidebar.js new file mode 100644 index 00000000000..da8969c80f3 --- /dev/null +++ b/app/assets/javascripts/issuable_init_bulk_update_sidebar.js @@ -0,0 +1,19 @@ +import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar'; +import issuableBulkUpdateActions from './issuable_bulk_update_actions'; + +export default { + bulkUpdateSidebar: null, + + init(prefixId) { + const bulkUpdateEl = document.querySelector('.issues-bulk-update'); + const alreadyInitialized = Boolean(this.bulkUpdateSidebar); + + if (bulkUpdateEl && !alreadyInitialized) { + issuableBulkUpdateActions.init({ prefixId }); + + this.bulkUpdateSidebar = new IssuableBulkUpdateSidebar(); + } + + return this.bulkUpdateSidebar; + }, +}; diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 3f954b43ee3..bea43430edc 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -11,7 +11,7 @@ import CreateLabelDropdown from './create_label'; import flash from './flash'; import ModalStore from './boards/stores/modal_store'; import boardsStore from './boards/stores/boards_store'; -import { isEE, isScopedLabel } from '~/lib/utils/common_utils'; +import { isScopedLabel } from '~/lib/utils/common_utils'; export default class LabelsSelect { constructor(els, options = {}) { @@ -140,7 +140,7 @@ export default class LabelsSelect { labelCount = data.labels.length; // EE Specific - if (isEE) { + if (IS_EE) { /** * For Scoped labels, the last label selected with the * same key will be applied to the current issueable. diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index cc5e12aa467..5e90893b684 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -726,14 +726,6 @@ export const NavigationType = { TYPE_RESERVED: 255, }; -/** - * Returns the value of `gon.ee` - * Used to check if it's the EE codebase or the CE one. - * - * @returns Boolean - */ -export const isEE = () => window.gon && window.gon.ee; - /** * Checks if the given Label has a special syntax `::` in * it's title. diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 9f30a989295..9e97f345717 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -33,6 +33,8 @@ import GlFieldErrors from './gl_field_errors'; import initUserPopovers from './user_popovers'; import { __ } from './locale'; +import 'ee_else_ce/main_ee'; + // expose jQuery as global (TODO: remove these) window.jQuery = jQuery; window.$ = jQuery; @@ -119,11 +121,15 @@ function deferredInitialisation() { .catch(() => {}); } + const glTooltipDelay = localStorage.getItem('gl-tooltip-delay'); + const delay = glTooltipDelay ? JSON.parse(glTooltipDelay) : 0; + // Initialize tooltips $body.tooltip({ selector: '.has-tooltip, [data-toggle="tooltip"]', trigger: 'hover', boundary: 'viewport', + delay, }); // Initialize popovers diff --git a/app/assets/javascripts/main_ee.js b/app/assets/javascripts/main_ee.js new file mode 100644 index 00000000000..84d74775163 --- /dev/null +++ b/app/assets/javascripts/main_ee.js @@ -0,0 +1 @@ +// This is an empty file to satisfy ee_else_ce import for the EE main entry point diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 2cbda8ea05d..ba79a697df2 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -4,7 +4,6 @@ import _ from 'underscore'; import { mapActions, mapState } from 'vuex'; import { s__ } from '~/locale'; import Icon from '~/vue_shared/components/icon.vue'; -import '~/vue_shared/mixins/is_ee'; import { getParameterValues } from '~/lib/utils/url_utility'; import invalidUrl from '~/lib/utils/invalid_url'; import MonitorAreaChart from './charts/area.vue'; @@ -124,6 +123,11 @@ export default { required: false, default: '', }, + smallEmptyState: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -155,6 +159,12 @@ export default { selectedDashboardText() { return this.currentDashboard || (this.allDashboards[0] && this.allDashboards[0].display_name); }, + addingMetricsAvailable() { + return IS_EE && this.canAddMetrics && !this.showEmptyState; + }, + alertWidgetAvailable() { + return IS_EE && this.prometheusAlertsAvailable && this.alertsEndpoint; + }, }, created() { this.setEndpoints({ @@ -308,7 +318,7 @@ export default {
-
+
diff --git a/app/assets/javascripts/monitoring/components/empty_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue index a3c6de14aa4..1bb40447a3e 100644 --- a/app/assets/javascripts/monitoring/components/empty_state.vue +++ b/app/assets/javascripts/monitoring/components/empty_state.vue @@ -1,7 +1,11 @@ diff --git a/app/assets/javascripts/monitoring/stores/utils.js b/app/assets/javascripts/monitoring/stores/utils.js index 84e1f1c4c20..721942f9d3b 100644 --- a/app/assets/javascripts/monitoring/stores/utils.js +++ b/app/assets/javascripts/monitoring/stores/utils.js @@ -36,15 +36,26 @@ function removeTimeSeriesNoData(queries) { // { metricId: 2, ...query2Attrs }] }, // { title: 'new title', y_label: 'MB', queries: [{ metricId: 3, ...query3Attrs }]} // ] -function groupQueriesByChartInfo(metrics) { +export function groupQueriesByChartInfo(metrics) { const metricsByChart = metrics.reduce((accumulator, metric) => { const { queries, ...chart } = metric; - const metricId = chart.id ? chart.id.toString() : null; const chartKey = `${chart.title}|${chart.y_label}`; accumulator[chartKey] = accumulator[chartKey] || { ...chart, queries: [] }; - queries.forEach(queryAttrs => accumulator[chartKey].queries.push({ metricId, ...queryAttrs })); + queries.forEach(queryAttrs => { + let metricId; + + if (chart.id) { + metricId = chart.id.toString(); + } else if (queryAttrs.metric_id) { + metricId = queryAttrs.metric_id.toString(); + } else { + metricId = null; + } + + accumulator[chartKey].queries.push({ metricId, ...queryAttrs }); + }); return accumulator; }, {}); diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 5a4b5f9398b..fda494fec07 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -13,6 +13,7 @@ import { splitCamelCase, slugifyWithUnderscore, } from '../../lib/utils/text_utility'; +import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; import * as constants from '../constants'; import eventHub from '../event_hub'; import issueWarning from '../../vue_shared/components/issue/issue_warning.vue'; @@ -65,14 +66,12 @@ export default { return this.getUserData.id; }, commentButtonTitle() { - return this.noteType === constants.COMMENT ? 'Comment' : 'Start thread'; + return this.noteType === constants.COMMENT ? __('Comment') : __('Start thread'); }, startDiscussionDescription() { - let text = 'Discuss a specific suggestion or question'; - if (this.getNoteableData.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE) { - text += ' that needs to be resolved'; - } - return `${text}.`; + return this.getNoteableData.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE + ? __('Discuss a specific suggestion or question that needs to be resolved.') + : __('Discuss a specific suggestion or question.'); }, isOpen() { return this.openState === constants.OPENED || this.openState === constants.REOPENED; @@ -127,8 +126,8 @@ export default { }, issuableTypeTitle() { return this.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE - ? 'merge request' - : 'issue'; + ? __('merge request') + : __('issue'); }, trackingLabel() { return slugifyWithUnderscore(`${this.commentButtonTitle} button`); @@ -203,7 +202,7 @@ export default { this.discard(); } else { Flash( - 'Something went wrong while adding your comment. Please try again.', + __('Something went wrong while adding your comment. Please try again.'), 'alert', this.$refs.commentForm, ); @@ -219,8 +218,9 @@ export default { .catch(() => { this.enableButton(); this.discard(false); - const msg = `Your comment could not be submitted! -Please check your network connection and try again.`; + const msg = __( + 'Your comment could not be submitted! Please check your network connection and try again.', + ); Flash(msg, 'alert', this.$el); this.note = noteData.data.note.note; // Restore textarea content. this.removePlaceholderNotes(); @@ -235,7 +235,10 @@ Please check your network connection and try again.`; toggleIssueState() { if (this.isOpen) { this.closeIssue() - .then(() => this.enableButton()) + .then(() => { + this.enableButton(); + refreshUserMergeRequestCounts(); + }) .catch(() => { this.enableButton(); this.toggleStateButtonLoading(false); @@ -248,7 +251,10 @@ Please check your network connection and try again.`; }); } else { this.reopenIssue() - .then(() => this.enableButton()) + .then(() => { + this.enableButton(); + refreshUserMergeRequestCounts(); + }) .catch(({ data }) => { this.enableButton(); this.toggleStateButtonLoading(false); @@ -298,7 +304,7 @@ Please check your network connection and try again.`; const noteableType = capitalizeFirstCharacter(convertToCamelCase(this.noteableType)); this.autosave = new Autosave($(this.$refs.textarea), [ - 'Note', + __('Note'), noteableType, this.getNoteableData.id, ]); @@ -359,8 +365,8 @@ Please check your network connection and try again.`; class="note-textarea js-vue-comment-form js-note-text js-gfm-input js-autosize markdown-area js-vue-textarea qa-comment-input" data-supports-quick-actions="true" - aria-label="Description" - placeholder="Write a comment or drag your files here…" + :aria-label="__('Description')" + :placeholder="__('Write a comment or drag your files here…')" @keydown.up="editCurrentUserLastNote()" @keydown.meta.enter="handleSave()" @keydown.ctrl.enter="handleSave()" @@ -381,7 +387,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" data-track-event="click_button" @click.prevent="handleSave()" > - {{ __(commentButtonTitle) }} + {{ commentButtonTitle }} @@ -404,8 +410,14 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" >
- Comment -

Add a general comment to this {{ noteableDisplayName }}.

+ {{ __('Comment') }} +

+ {{ + sprintf(__('Add a general comment to this %{noteableDisplayName}.'), { + noteableDisplayName, + }) + }} +

@@ -418,7 +430,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" >
- Start thread + {{ __('Start thread') }}

{{ startDiscussionDescription }}

diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue index 54c242b2fda..164e79c6294 100644 --- a/app/assets/javascripts/notes/components/diff_with_note.vue +++ b/app/assets/javascripts/notes/components/diff_with_note.vue @@ -100,7 +100,7 @@ export default { class="btn-link btn-link-retry btn-no-padding js-toggle-lazy-diff-retry-button" @click="fetchDiff" > - Try again + {{ __('Try again') }} diff --git a/app/assets/javascripts/notes/components/discussion_notes.vue b/app/assets/javascripts/notes/components/discussion_notes.vue index 2ff0fee62f3..0b136549c14 100644 --- a/app/assets/javascripts/notes/components/discussion_notes.vue +++ b/app/assets/javascripts/notes/components/discussion_notes.vue @@ -8,12 +8,14 @@ import SystemNote from '~/vue_shared/components/notes/system_note.vue'; import NoteableNote from './noteable_note.vue'; import ToggleRepliesWidget from './toggle_replies_widget.vue'; import NoteEditedText from './note_edited_text.vue'; +import DiscussionNotesRepliesWrapper from './discussion_notes_replies_wrapper.vue'; export default { name: 'DiscussionNotes', components: { ToggleRepliesWidget, NoteEditedText, + DiscussionNotesRepliesWrapper, }, props: { discussion: { @@ -119,9 +121,7 @@ export default { /> -
+ -
+