diff options
Diffstat (limited to 'app/assets')
163 files changed, 1838 insertions, 742 deletions
diff --git a/app/assets/images/auth_buttons/salesforce_64.png b/app/assets/images/auth_buttons/salesforce_64.png Binary files differnew file mode 100644 index 00000000000..c8a86a0c515 --- /dev/null +++ b/app/assets/images/auth_buttons/salesforce_64.png 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 { <div class="board-blank-state p-3"> <p> {{ - __('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:') }} </p> <ul class="list-unstyled board-blank-state-list"> @@ -76,7 +76,7 @@ export default { </ul> <p> {{ - __( + 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') }} </button> <button class="btn btn-default btn-block" type="button" @click.stop="clearBlankState"> - {{ __("BoardBlankState|Nevermind, I'll use my own") }} + {{ s__("BoardBlankState|Nevermind, I'll use my own") }} </button> </div> </template> 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 */ -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`; - } +import boardsStore from '~/boards/stores/boards_store'; +export default class BoardService { 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 670e8e9eb60..7dbaf984acf 100644 --- a/app/assets/javascripts/branches/divergence_graph.js +++ b/app/assets/javascripts/branches/divergence_graph.js @@ -1,23 +1,51 @@ import Vue from 'vue'; +import { __ } from '../locale'; +import createFlash from '../flash'; +import axios from '../lib/utils/axios_utils'; import DivergenceGraph from './components/divergence_graph.vue'; -export default () => { - document.querySelectorAll('.js-branch-divergence-graph').forEach(el => { - const { distance, aheadCount, behindCount, defaultBranch, maxCommits } = el.dataset; - - return new Vue({ - el, - render(h) { - return h(DivergenceGraph, { - props: { - defaultBranch, - distance: distance ? parseInt(distance, 10) : null, - aheadCount: parseInt(aheadCount, 10), - behindCount: parseInt(behindCount, 10), - maxCommits: parseInt(maxCommits, 10), - }, - }); - }, - }); +export function createGraphVueApp(el, data, maxCommits) { + return new Vue({ + el, + render(h) { + return h(DivergenceGraph, { + props: { + defaultBranch: 'master', + distance: data.distance ? parseInt(data.distance, 10) : null, + aheadCount: parseInt(data.ahead, 10), + behindCount: parseInt(data.behind, 10), + maxCommits, + }, + }); + }, }); +} + +export default endpoint => { + const names = [...document.querySelectorAll('.js-branch-item')].map( + ({ dataset }) => dataset.name, + ); + return axios + .get(endpoint, { + params: { names }, + }) + .then(({ data }) => { + const maxCommits = Object.entries(data).reduce((acc, [, val]) => { + const max = Math.max(...Object.values(val)); + return max > acc ? max : acc; + }, 100); + + Object.entries(data).forEach(([branchName, val]) => { + const el = document.querySelector( + `[data-name="${branchName}"] .js-branch-divergence-graph`, + ); + + if (!el) return; + + createGraphVueApp(el, val, maxCommits); + }); + }) + .catch(() => + createFlash(__('Error fetching diverging counts for branches. Please try again.')), + ); }; diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue index 4771090aa7e..cd2121db3b2 100644 --- a/app/assets/javascripts/clusters/components/application_row.vue +++ b/app/assets/javascripts/clusters/components/application_row.vue @@ -207,7 +207,7 @@ export default { return __('Updating'); } - return __('Updated'); + return this.updateSuccessful ? __('Updated to') : __('Updated'); }, updateFailureDescription() { return s__('ClusterIntegration|Update failed. Please check the logs and try again.'); @@ -331,8 +331,6 @@ export default { class="form-text text-muted label p-0 js-cluster-application-update-details" > {{ versionLabel }} - <span v-if="updateSuccessful">to</span> - <gl-link v-if="updateSuccessful" :href="chartRepo" diff --git a/app/assets/javascripts/clusters/components/knative_domain_editor.vue b/app/assets/javascripts/clusters/components/knative_domain_editor.vue index 480228619a5..e26ef135bc5 100644 --- a/app/assets/javascripts/clusters/components/knative_domain_editor.vue +++ b/app/assets/javascripts/clusters/components/knative_domain_editor.vue @@ -2,7 +2,7 @@ import LoadingButton from '~/vue_shared/components/loading_button.vue'; import ClipboardButton from '../../vue_shared/components/clipboard_button.vue'; import { GlLoadingIcon } from '@gitlab/ui'; -import { s__ } from '~/locale'; +import { __, s__ } from '~/locale'; import { APPLICATION_STATUS } from '~/clusters/constants'; @@ -32,7 +32,7 @@ export default { return [UPDATING].includes(this.knative.status); }, saveButtonLabel() { - return this.saving ? this.__('Saving') : this.__('Save changes'); + return this.saving ? __('Saving') : __('Save changes'); }, knativeInstalled() { return this.knative.installed; @@ -122,9 +122,9 @@ export default { `ClusterIntegration|To access your application after deployment, point a wildcard DNS to the Knative Endpoint.`, ) }} - <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer"> - {{ __('More information') }} - </a> + <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{ + __('More information') + }}</a> </p> <p diff --git a/app/assets/javascripts/clusters/components/uninstall_application_button.vue b/app/assets/javascripts/clusters/components/uninstall_application_button.vue index ef4bcbe14dd..8465312d84d 100644 --- a/app/assets/javascripts/clusters/components/uninstall_application_button.vue +++ b/app/assets/javascripts/clusters/components/uninstall_application_button.vue @@ -1,6 +1,7 @@ <script> import LoadingButton from '~/vue_shared/components/loading_button.vue'; import { APPLICATION_STATUS } from '~/clusters/constants'; +import { __ } from '~/locale'; const { UPDATING, UNINSTALLING } = APPLICATION_STATUS; @@ -22,7 +23,7 @@ export default { return this.status === UNINSTALLING; }, label() { - return this.loading ? this.__('Uninstalling') : this.__('Uninstall'); + return this.loading ? __('Uninstalling') : __('Uninstall'); }, }, }; diff --git a/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue b/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue index 65827f1cb6a..920439ebb23 100644 --- a/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue +++ b/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue @@ -14,7 +14,9 @@ const CUSTOM_APP_WARNING_TEXT = { [PROMETHEUS]: s__('ClusterIntegration|All data will be deleted and cannot be restored.'), [RUNNER]: s__('ClusterIntegration|Any running pipelines will be canceled.'), [KNATIVE]: s__('ClusterIntegration|The associated IP will be deleted and cannot be restored.'), - [JUPYTER]: '', + [JUPYTER]: s__( + 'ClusterIntegration|All data not committed to GitLab will be deleted and cannot be restored.', + ), }; export default { 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 @@ +<script> +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { __ } from '~/locale'; +import Icon from '~/vue_shared/components/icon.vue'; + +export default { + components: { + GlDropdown, + GlDropdownItem, + Icon, + }, + props: { + projects: { + type: Array, + required: true, + }, + selectedProject: { + type: Object, + required: false, + default: () => ({}), + }, + }, + computed: { + dropdownText() { + if (Object.keys(this.selectedProject).length) { + return this.selectedProject.name; + } + + return __('Select private project'); + }, + }, + methods: { + selectProject(project) { + this.$emit('click', project); + }, + }, +}; +</script> + +<template> + <gl-dropdown toggle-class="d-flex align-items-center w-100" class="w-100"> + <template slot="button-content"> + <span class="str-truncated-100 mr-2"> + <icon name="lock" /> + {{ dropdownText }} + </span> + <icon name="chevron-down" class="ml-auto" /> + </template> + <gl-dropdown-item v-for="project in projects" :key="project.id" @click="selectProject(project)"> + <icon + name="mobile-issue-close" + :class="{ icon: project.id !== selectedProject.id }" + class="js-active-project-check" + /> + <span class="ml-1">{{ project.name }}</span> + </gl-dropdown-item> + </gl-dropdown> +</template> 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 @@ +<script> +import { GlLink } from '@gitlab/ui'; +import { __, sprintf } from '../../locale'; +import createFlash from '../../flash'; +import Api from '../../api'; +import state from '../state'; +import Dropdown from './dropdown.vue'; + +export default { + components: { + GlLink, + Dropdown, + }, + props: { + namespacePath: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: true, + }, + newForkPath: { + type: String, + required: true, + }, + helpPagePath: { + type: String, + required: true, + }, + }, + data() { + return { + projects: [], + }; + }, + computed: { + selectedProject() { + return state.selectedProject; + }, + noForkText() { + return sprintf( + __( + "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private.", + ), + { link_start: `<a href="${this.newForkPath}" class="help-link">`, link_end: '</a>' }, + false, + ); + }, + }, + mounted() { + this.fetchProjects(); + this.createBtn = document.querySelector('.js-create-target'); + this.warningText = document.querySelector('.js-exposed-info-warning'); + }, + methods: { + selectProject(project) { + if (project) { + Object.assign(state, { + selectedProject: project, + }); + + if (project.namespaceFullPath !== this.namespacePath) { + this.showWarning(); + } + } else if (this.createBtn) { + this.createBtn.setAttribute('disabled', 'disabled'); + } + }, + normalizeProjectData(data) { + return data.map(p => ({ + id: p.id, + name: p.name_with_namespace, + pathWithNamespace: p.path_with_namespace, + namespaceFullpath: p.namespace.full_path, + })); + }, + fetchProjects() { + Api.projectForks(this.projectPath, { + with_merge_requests_enabled: true, + min_access_level: 30, + visibility: 'private', + }) + .then(({ data }) => { + this.projects = this.normalizeProjectData(data); + this.selectProject(this.projects[0]); + }) + .catch(e => { + createFlash(__('Error fetching forked projects. Please try again.')); + throw e; + }); + }, + showWarning() { + if (this.warningText) { + this.warningText.classList.remove('hidden'); + } + + if (this.createBtn) { + this.createBtn.classList.add('btn-warning'); + this.createBtn.classList.remove('btn-success'); + } + }, + }, +}; +</script> + +<template> + <div class="form-group"> + <label>{{ __('Project') }}</label> + <div> + <dropdown + v-if="projects.length" + :projects="projects" + :selected-project="selectedProject" + @click="selectProject" + /> + <p class="text-muted mt-1 mb-0"> + <template v-if="projects.length"> + {{ + __( + "To protect this issue's confidentiality, a private fork of this project was selected.", + ) + }} + </template> + <template v-else> + {{ __('No forks available to you.') }}<br /> + <span v-html="noForkText"></span> + </template> + </p> + </div> + </div> +</template> 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/diff_notes/components/comment_resolve_btn.js b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js index f66e07ba31a..7817b41514d 100644 --- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js +++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js @@ -32,15 +32,15 @@ const CommentAndResolveBtn = Vue.extend({ buttonText: function() { if (this.isDiscussionResolved) { if (this.textareaIsEmpty) { - return __('Unresolve discussion'); + return __('Unresolve thread'); } else { - return __('Comment & unresolve discussion'); + return __('Comment & unresolve thread'); } } else { if (this.textareaIsEmpty) { - return __('Resolve discussion'); + return __('Resolve thread'); } else { - return __('Comment & resolve discussion'); + return __('Comment & resolve thread'); } } }, diff --git a/app/assets/javascripts/diffs/components/diff_discussion_reply.vue b/app/assets/javascripts/diffs/components/diff_discussion_reply.vue new file mode 100644 index 00000000000..2aa5e9b3339 --- /dev/null +++ b/app/assets/javascripts/diffs/components/diff_discussion_reply.vue @@ -0,0 +1,55 @@ +<script> +import { mapGetters } from 'vuex'; +import NoteSignedOutWidget from '~/notes/components/note_signed_out_widget.vue'; +import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue'; +import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; + +export default { + name: 'DiffDiscussionReply', + components: { + NoteSignedOutWidget, + ReplyPlaceholder, + UserAvatarLink, + }, + props: { + hasForm: { + type: Boolean, + required: false, + default: false, + }, + renderReplyPlaceholder: { + type: Boolean, + required: true, + }, + }, + computed: { + ...mapGetters({ + currentUser: 'getUserData', + userCanReply: 'userCanReply', + }), + }, +}; +</script> + +<template> + <div class="discussion-reply-holder d-flex clearfix"> + <template v-if="userCanReply"> + <slot v-if="hasForm" name="form"></slot> + <template v-else-if="renderReplyPlaceholder"> + <user-avatar-link + :link-href="currentUser.path" + :img-src="currentUser.avatar_url" + :img-alt="currentUser.name" + :img-size="40" + class="d-none d-sm-block" + /> + <reply-placeholder + class="qa-discussion-reply" + :button-text="__('Start a new discussion...')" + @onClick="$emit('showNewDiscussionForm')" + /> + </template> + </template> + <note-signed-out-widget v-else /> + </div> +</template> diff --git a/app/assets/javascripts/diffs/components/diff_discussions.vue b/app/assets/javascripts/diffs/components/diff_discussions.vue index 4c73eea4049..b0460bacff2 100644 --- a/app/assets/javascripts/diffs/components/diff_discussions.vue +++ b/app/assets/javascripts/diffs/components/diff_discussions.vue @@ -80,7 +80,6 @@ export default { v-show="isExpanded(discussion)" :discussion="discussion" :render-diff-file="false" - :always-expanded="true" :discussions-by-diff-order="true" :line="line" :help-page-path="helpPagePath" diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index eb9f1465945..4b226e30699 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -151,7 +151,11 @@ export default { stickyMonitor(this.$refs.header, contentTop() - fileHeaderHeight - 1, false); }, methods: { - ...mapActions('diffs', ['toggleFileDiscussions', 'toggleFullDiff']), + ...mapActions('diffs', [ + 'toggleFileDiscussions', + 'toggleFileDiscussionWrappers', + 'toggleFullDiff', + ]), handleToggleFile(e, checkTarget) { if ( !checkTarget || @@ -165,7 +169,7 @@ export default { this.$emit('showForkMessage'); }, handleToggleDiscussions() { - this.toggleFileDiscussions(this.diffFile); + this.toggleFileDiscussionWrappers(this.diffFile); }, handleFileNameClick(e) { const isLinkToOtherPage = diff --git a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue index e28909b7be3..af5550aec3b 100644 --- a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue +++ b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue @@ -1,5 +1,4 @@ <script> -import { mapActions } from 'vuex'; import Icon from '~/vue_shared/components/icon.vue'; import { pluralize, truncate } from '~/lib/utils/text_utility'; import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; @@ -19,11 +18,13 @@ export default { type: Array, required: true, }, + discussionsExpanded: { + type: Boolean, + required: false, + default: false, + }, }, computed: { - discussionsExpanded() { - return this.discussions.every(discussion => discussion.expanded); - }, allDiscussions() { return this.discussions.reduce((acc, note) => acc.concat(note.notes), []); }, @@ -45,26 +46,14 @@ export default { }, }, methods: { - ...mapActions(['toggleDiscussion']), getTooltipText(noteData) { let { note } = noteData; - if (note.length > LENGTH_OF_AVATAR_TOOLTIP) { note = truncate(note, LENGTH_OF_AVATAR_TOOLTIP); } return `${noteData.author.name}: ${note}`; }, - toggleDiscussions() { - const forceExpanded = this.discussions.some(discussion => !discussion.expanded); - - this.discussions.forEach(discussion => { - this.toggleDiscussion({ - discussionId: discussion.id, - forceExpanded, - }); - }); - }, }, }; </script> @@ -76,7 +65,7 @@ export default { type="button" :aria-label="__('Show comments')" class="diff-notes-collapse js-diff-comment-avatar js-diff-comment-button" - @click="toggleDiscussions" + @click="$emit('toggleLineDiscussions')" > <icon :size="12" name="collapse" /> </button> @@ -87,7 +76,7 @@ export default { :img-src="note.author.avatar_url" :tooltip-text="getTooltipText(note)" class="diff-comment-avatar js-diff-comment-avatar" - @click.native="toggleDiscussions" + @click.native="$emit('toggleLineDiscussions')" /> <span v-if="moreText" @@ -97,7 +86,7 @@ export default { data-container="body" data-placement="top" role="button" - @click="toggleDiscussions" + @click="$emit('toggleLineDiscussions')" >+{{ moreCount }}</span > </template> diff --git a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue index 1281f9b17ef..351110f0a87 100644 --- a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue +++ b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue @@ -105,7 +105,13 @@ export default { }, }, methods: { - ...mapActions('diffs', ['loadMoreLines', 'showCommentForm', 'setHighlightedRow']), + ...mapActions('diffs', [ + 'loadMoreLines', + 'showCommentForm', + 'setHighlightedRow', + 'toggleLineDiscussions', + 'toggleLineDiscussionWrappers', + ]), handleCommentButton() { this.showCommentForm({ lineCode: this.line.line_code, fileHash: this.fileHash }); }, @@ -184,7 +190,14 @@ export default { @click="setHighlightedRow(lineCode)" > </a> - <diff-gutter-avatars v-if="shouldShowAvatarsOnGutter" :discussions="line.discussions" /> + <diff-gutter-avatars + v-if="shouldShowAvatarsOnGutter" + :discussions="line.discussions" + :discussions-expanded="line.discussionsExpanded" + @toggleLineDiscussions=" + toggleLineDiscussions({ lineCode, fileHash, expanded: !line.discussionsExpanded }) + " + /> </template> </div> </template> 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 1faa0493e79..a06dbd70ac5 100644 --- a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue +++ b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue @@ -1,11 +1,14 @@ <script> -import diffDiscussions from './diff_discussions.vue'; -import diffLineNoteForm from './diff_line_note_form.vue'; +import { mapActions } from 'vuex'; +import DiffDiscussions from './diff_discussions.vue'; +import DiffLineNoteForm from './diff_line_note_form.vue'; +import DiffDiscussionReply from './diff_discussion_reply.vue'; export default { components: { - diffDiscussions, - diffLineNoteForm, + DiffDiscussions, + DiffLineNoteForm, + DiffDiscussionReply, }, props: { line: { @@ -21,6 +24,11 @@ export default { required: false, default: '', }, + hasDraft: { + type: Boolean, + required: false, + default: false, + }, }, computed: { className() { @@ -32,10 +40,12 @@ export default { if (!this.line.discussions || !this.line.discussions.length) { return false; } - - return this.line.discussions.every(discussion => discussion.expanded); + return this.line.discussionsExpanded; }, }, + methods: { + ...mapActions('diffs', ['showCommentForm']), + }, }; </script> @@ -49,13 +59,23 @@ export default { :discussions="line.discussions" :help-page-path="helpPagePath" /> - <diff-line-note-form - v-if="line.hasForm" - :diff-file-hash="diffFileHash" - :line="line" - :note-target-line="line" - :help-page-path="helpPagePath" - /> + <diff-discussion-reply + v-if="!hasDraft" + :has-form="line.hasForm" + :render-reply-placeholder="Boolean(line.discussions.length)" + @showNewDiscussionForm=" + showCommentForm({ lineCode: line.line_code, fileHash: diffFileHash }) + " + > + <template #form> + <diff-line-note-form + :diff-file-hash="diffFileHash" + :line="line" + :note-target-line="line" + :help-page-path="helpPagePath" + /> + </template> + </diff-discussion-reply> </div> </td> </tr> diff --git a/app/assets/javascripts/diffs/components/inline_diff_view.vue b/app/assets/javascripts/diffs/components/inline_diff_view.vue index 8c76a555b62..b2bc3d9914f 100644 --- a/app/assets/javascripts/diffs/components/inline_diff_view.vue +++ b/app/assets/javascripts/diffs/components/inline_diff_view.vue @@ -57,6 +57,7 @@ export default { :diff-file-hash="diffFile.file_hash" :line="line" :help-page-path="helpPagePath" + :has-draft="shouldRenderDraftRow(diffFile.file_hash, line) || false" /> <inline-draft-comment-row v-if="shouldRenderDraftRow(diffFile.file_hash, line)" diff --git a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue index d2e54edca85..65b41b0e456 100644 --- a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue +++ b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue @@ -1,11 +1,14 @@ <script> -import diffDiscussions from './diff_discussions.vue'; -import diffLineNoteForm from './diff_line_note_form.vue'; +import { mapActions } from 'vuex'; +import DiffDiscussions from './diff_discussions.vue'; +import DiffLineNoteForm from './diff_line_note_form.vue'; +import DiffDiscussionReply from './diff_discussion_reply.vue'; export default { components: { - diffDiscussions, - diffLineNoteForm, + DiffDiscussions, + DiffLineNoteForm, + DiffDiscussionReply, }, props: { line: { @@ -25,28 +28,44 @@ export default { required: false, default: '', }, + hasDraftLeft: { + type: Boolean, + required: false, + default: false, + }, + hasDraftRight: { + type: Boolean, + required: false, + default: false, + }, }, computed: { hasExpandedDiscussionOnLeft() { return this.line.left && this.line.left.discussions.length - ? this.line.left.discussions.every(discussion => discussion.expanded) + ? this.line.left.discussionsExpanded : false; }, hasExpandedDiscussionOnRight() { return this.line.right && this.line.right.discussions.length - ? this.line.right.discussions.every(discussion => discussion.expanded) + ? this.line.right.discussionsExpanded : false; }, hasAnyExpandedDiscussion() { return this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight; }, shouldRenderDiscussionsOnLeft() { - return this.line.left && this.line.left.discussions && this.hasExpandedDiscussionOnLeft; + return ( + this.line.left && + this.line.left.discussions && + this.line.left.discussions.length && + this.hasExpandedDiscussionOnLeft + ); }, shouldRenderDiscussionsOnRight() { return ( this.line.right && this.line.right.discussions && + this.line.right.discussions.length && this.hasExpandedDiscussionOnRight && this.line.right.type ); @@ -81,6 +100,22 @@ export default { return hasCommentFormOnLeft || hasCommentFormOnRight; }, + shouldRenderReplyPlaceholderOnLeft() { + return Boolean( + this.line.left && this.line.left.discussions && this.line.left.discussions.length, + ); + }, + shouldRenderReplyPlaceholderOnRight() { + return Boolean( + this.line.right && this.line.right.discussions && this.line.right.discussions.length, + ); + }, + }, + methods: { + ...mapActions('diffs', ['showCommentForm']), + showNewDiscussionForm() { + this.showCommentForm({ lineCode: this.line.line_code, fileHash: this.diffFileHash }); + }, }, }; </script> @@ -90,37 +125,51 @@ export default { <td class="notes-content parallel old" colspan="2"> <div v-if="shouldRenderDiscussionsOnLeft" class="content"> <diff-discussions - v-if="line.left.discussions.length" :discussions="line.left.discussions" :line="line.left" :help-page-path="helpPagePath" /> </div> - <diff-line-note-form - v-if="showLeftSideCommentForm" - :diff-file-hash="diffFileHash" - :line="line.left" - :note-target-line="line.left" - :help-page-path="helpPagePath" - line-position="left" - /> + <diff-discussion-reply + v-if="!hasDraftLeft" + :has-form="showLeftSideCommentForm" + :render-reply-placeholder="shouldRenderReplyPlaceholderOnLeft" + @showNewDiscussionForm="showNewDiscussionForm" + > + <template #form> + <diff-line-note-form + :diff-file-hash="diffFileHash" + :line="line.left" + :note-target-line="line.left" + :help-page-path="helpPagePath" + line-position="left" + /> + </template> + </diff-discussion-reply> </td> <td class="notes-content parallel new" colspan="2"> <div v-if="shouldRenderDiscussionsOnRight" class="content"> <diff-discussions - v-if="line.right.discussions.length" :discussions="line.right.discussions" :line="line.right" :help-page-path="helpPagePath" /> </div> - <diff-line-note-form - v-if="showRightSideCommentForm" - :diff-file-hash="diffFileHash" - :line="line.right" - :note-target-line="line.right" - line-position="right" - /> + <diff-discussion-reply + v-if="!hasDraftRight" + :has-form="showRightSideCommentForm" + :render-reply-placeholder="shouldRenderReplyPlaceholderOnRight" + @showNewDiscussionForm="showNewDiscussionForm" + > + <template #form> + <diff-line-note-form + :diff-file-hash="diffFileHash" + :line="line.right" + :note-target-line="line.right" + line-position="right" + /> + </template> + </diff-discussion-reply> </td> </tr> </template> diff --git a/app/assets/javascripts/diffs/components/parallel_diff_view.vue b/app/assets/javascripts/diffs/components/parallel_diff_view.vue index 41a80d99850..c477e68c33c 100644 --- a/app/assets/javascripts/diffs/components/parallel_diff_view.vue +++ b/app/assets/javascripts/diffs/components/parallel_diff_view.vue @@ -58,6 +58,8 @@ export default { :diff-file-hash="diffFile.file_hash" :line-index="index" :help-page-path="helpPagePath" + :has-draft-left="hasParallelDraftLeft(diffFile.file_hash, line) || false" + :has-draft-right="hasParallelDraftRight(diffFile.file_hash, line) || false" /> <parallel-draft-comment-row v-if="shouldRenderParallelDraftRow(diffFile.file_hash, line)" diff --git a/app/assets/javascripts/diffs/mixins/draft_comments.js b/app/assets/javascripts/diffs/mixins/draft_comments.js index dfb71bf38ce..b6c9b132aeb 100644 --- a/app/assets/javascripts/diffs/mixins/draft_comments.js +++ b/app/assets/javascripts/diffs/mixins/draft_comments.js @@ -6,5 +6,7 @@ export default { imageDiscussions() { return this.diffFile.discussions; }, + hasParallelDraftLeft: () => () => false, + hasParallelDraftRight: () => () => false, }, }; diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 88d7b4bba63..32e0d8f42ee 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -12,6 +12,7 @@ import { getNoteFormData, convertExpandLines, idleCallback, + allDiscussionWrappersExpanded, } from './utils'; import * as types from './mutation_types'; import { @@ -79,6 +80,7 @@ export const assignDiscussionsToDiff = ( discussions = rootState.notes.discussions, ) => { const diffPositionByLineCode = getDiffPositionByLineCode(state.diffFiles); + const hash = getLocationHash(); discussions .filter(discussion => discussion.diff_discussion) @@ -86,6 +88,7 @@ export const assignDiscussionsToDiff = ( commit(types.SET_LINE_DISCUSSIONS_FOR_FILE, { discussion, diffPositionByLineCode, + hash, }); }); @@ -99,6 +102,10 @@ export const removeDiscussionsFromDiff = ({ commit }, removeDiscussion) => { commit(types.REMOVE_LINE_DISCUSSIONS_FOR_FILE, { fileHash: file_hash, lineCode: line_code, id }); }; +export const toggleLineDiscussions = ({ commit }, options) => { + commit(types.TOGGLE_LINE_DISCUSSIONS, options); +}; + export const renderFileForDiscussionId = ({ commit, rootState, state }, discussionId) => { const discussion = rootState.notes.discussions.find(d => d.id === discussionId); @@ -257,6 +264,31 @@ export const toggleFileDiscussions = ({ getters, dispatch }, diff) => { }); }; +export const toggleFileDiscussionWrappers = ({ commit }, diff) => { + const discussionWrappersExpanded = allDiscussionWrappersExpanded(diff); + let linesWithDiscussions; + if (diff.highlighted_diff_lines) { + linesWithDiscussions = diff.highlighted_diff_lines.filter(line => line.discussions.length); + } + if (diff.parallel_diff_lines) { + linesWithDiscussions = diff.parallel_diff_lines.filter( + line => + (line.left && line.left.discussions.length) || + (line.right && line.right.discussions.length), + ); + } + + if (linesWithDiscussions.length) { + linesWithDiscussions.forEach(line => { + commit(types.TOGGLE_LINE_DISCUSSIONS, { + fileHash: diff.file_hash, + lineCode: line.line_code, + expanded: !discussionWrappersExpanded, + }); + }); + } +}; + export const saveDiffDiscussion = ({ state, dispatch }, { note, formData }) => { const postData = getNoteFormData({ commit: state.commit, @@ -267,7 +299,7 @@ export const saveDiffDiscussion = ({ state, dispatch }, { note, formData }) => { return dispatch('saveNote', postData, { root: true }) .then(result => dispatch('updateDiscussion', result.discussion, { root: true })) .then(discussion => dispatch('assignDiscussionsToDiff', [discussion])) - .then(() => dispatch('updateResolvableDiscussonsCounts', null, { root: true })) + .then(() => dispatch('updateResolvableDiscussionsCounts', null, { root: true })) .then(() => dispatch('closeDiffFileCommentForm', formData.diffFile.file_hash)) .catch(() => createFlash(s__('MergeRequests|Saving the comment failed'))); }; diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js index 8d6111da500..9db56331faa 100644 --- a/app/assets/javascripts/diffs/store/mutation_types.js +++ b/app/assets/javascripts/diffs/store/mutation_types.js @@ -35,3 +35,5 @@ export const ADD_CURRENT_VIEW_DIFF_FILE_LINES = 'ADD_CURRENT_VIEW_DIFF_FILE_LINE export const TOGGLE_DIFF_FILE_RENDERING_MORE = 'TOGGLE_DIFF_FILE_RENDERING_MORE'; export const SET_SHOW_SUGGEST_POPOVER = 'SET_SHOW_SUGGEST_POPOVER'; + +export const TOGGLE_LINE_DISCUSSIONS = 'TOGGLE_LINE_DISCUSSIONS'; diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js index 00181a63c43..a66f205bbbd 100644 --- a/app/assets/javascripts/diffs/store/mutations.js +++ b/app/assets/javascripts/diffs/store/mutations.js @@ -6,6 +6,7 @@ import { addContextLines, prepareDiffData, isDiscussionApplicableToLine, + updateLineInFile, } from './utils'; import * as types from './mutation_types'; @@ -109,7 +110,7 @@ export default { })); }, - [types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { discussion, diffPositionByLineCode }) { + [types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { discussion, diffPositionByLineCode, hash }) { const { latestDiff } = state; const discussionLineCode = discussion.line_code; @@ -130,13 +131,27 @@ export default { : [], }); + const setDiscussionsExpanded = line => { + const isLineNoteTargeted = line.discussions.some( + disc => disc.notes && disc.notes.find(note => hash === `note_${note.id}`), + ); + + return { + ...line, + discussionsExpanded: + line.discussions && line.discussions.length + ? line.discussions.some(disc => !disc.resolved) || isLineNoteTargeted + : false, + }; + }; + state.diffFiles = state.diffFiles.map(diffFile => { if (diffFile.file_hash === fileHash) { const file = { ...diffFile }; if (file.highlighted_diff_lines) { file.highlighted_diff_lines = file.highlighted_diff_lines.map(line => - lineCheck(line) ? mapDiscussions(line) : line, + setDiscussionsExpanded(lineCheck(line) ? mapDiscussions(line) : line), ); } @@ -148,8 +163,10 @@ export default { if (left || right) { return { ...line, - left: line.left ? mapDiscussions(line.left) : null, - right: line.right ? mapDiscussions(line.right, () => !left) : null, + left: line.left ? setDiscussionsExpanded(mapDiscussions(line.left)) : null, + right: line.right + ? setDiscussionsExpanded(mapDiscussions(line.right, () => !left)) + : null, }; } @@ -173,32 +190,11 @@ export default { [types.REMOVE_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, lineCode }) { const selectedFile = state.diffFiles.find(f => f.file_hash === fileHash); if (selectedFile) { - if (selectedFile.parallel_diff_lines) { - const targetLine = selectedFile.parallel_diff_lines.find( - line => - (line.left && line.left.line_code === lineCode) || - (line.right && line.right.line_code === lineCode), - ); - if (targetLine) { - const side = targetLine.left && targetLine.left.line_code === lineCode ? 'left' : 'right'; - - Object.assign(targetLine[side], { - discussions: targetLine[side].discussions.filter(discussion => discussion.notes.length), - }); - } - } - - if (selectedFile.highlighted_diff_lines) { - const targetInlineLine = selectedFile.highlighted_diff_lines.find( - line => line.line_code === lineCode, - ); - - if (targetInlineLine) { - Object.assign(targetInlineLine, { - discussions: targetInlineLine.discussions.filter(discussion => discussion.notes.length), - }); - } - } + updateLineInFile(selectedFile, lineCode, line => + Object.assign(line, { + discussions: line.discussions.filter(discussion => discussion.notes.length), + }), + ); if (selectedFile.discussions && selectedFile.discussions.length) { selectedFile.discussions = selectedFile.discussions.filter( @@ -207,6 +203,15 @@ export default { } } }, + + [types.TOGGLE_LINE_DISCUSSIONS](state, { fileHash, lineCode, expanded }) { + const selectedFile = state.diffFiles.find(f => f.file_hash === fileHash); + + updateLineInFile(selectedFile, lineCode, line => + Object.assign(line, { discussionsExpanded: expanded }), + ); + }, + [types.TOGGLE_FOLDER_OPEN](state, path) { state.treeEntries[path].opened = !state.treeEntries[path].opened; }, diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js index 71956255eef..1c3ed84001c 100644 --- a/app/assets/javascripts/diffs/store/utils.js +++ b/app/assets/javascripts/diffs/store/utils.js @@ -454,3 +454,48 @@ export const convertExpandLines = ({ }; export const idleCallback = cb => requestIdleCallback(cb); + +export const updateLineInFile = (selectedFile, lineCode, updateFn) => { + if (selectedFile.parallel_diff_lines) { + const targetLine = selectedFile.parallel_diff_lines.find( + line => + (line.left && line.left.line_code === lineCode) || + (line.right && line.right.line_code === lineCode), + ); + if (targetLine) { + const side = targetLine.left && targetLine.left.line_code === lineCode ? 'left' : 'right'; + + updateFn(targetLine[side]); + } + } + if (selectedFile.highlighted_diff_lines) { + const targetInlineLine = selectedFile.highlighted_diff_lines.find( + line => line.line_code === lineCode, + ); + + if (targetInlineLine) { + updateFn(targetInlineLine); + } + } +}; + +export const allDiscussionWrappersExpanded = diff => { + const discussionsExpandedArray = []; + if (diff.parallel_diff_lines) { + diff.parallel_diff_lines.forEach(line => { + if (line.left && line.left.discussions.length) { + discussionsExpandedArray.push(line.left.discussionsExpanded); + } + if (line.right && line.right.discussions.length) { + discussionsExpandedArray.push(line.right.discussionsExpanded); + } + }); + } else if (diff.highlighted_diff_lines) { + diff.parallel_diff_lines.forEach(line => { + if (line.discussions.length) { + discussionsExpandedArray.push(line.discussionsExpanded); + } + }); + } + return discussionsExpandedArray.every(el => el); +}; diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue index 208bd19f6b0..21244c14977 100644 --- a/app/assets/javascripts/environments/components/environment_actions.vue +++ b/app/assets/javascripts/environments/components/environment_actions.vue @@ -1,5 +1,5 @@ <script> -import { s__, sprintf } from '~/locale'; +import { __, s__, sprintf } from '~/locale'; import { formatTime } from '~/lib/utils/datetime_utility'; import Icon from '~/vue_shared/components/icon.vue'; import eventHub from '../event_hub'; @@ -28,7 +28,7 @@ export default { }, computed: { title() { - return 'Deploy to...'; + return __('Deploy to...'); }, }, methods: { @@ -80,7 +80,8 @@ export default { data-toggle="dropdown" > <span> - <icon name="play" /> <icon name="chevron-down" /> + <icon name="play" /> + <icon name="chevron-down" /> <gl-loading-icon v-if="isLoading" /> </span> </button> @@ -94,9 +95,10 @@ export default { class="js-manual-action-link no-btn btn d-flex align-items-center" @click="onClickAction(action)" > - <span class="flex-fill"> {{ action.name }} </span> + <span class="flex-fill">{{ action.name }}</span> <span v-if="action.scheduledAt" class="text-secondary"> - <icon name="clock" /> {{ remainingTime(action) }} + <icon name="clock" /> + {{ remainingTime(action) }} </span> </button> </li> diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index f0e80cba753..813045cb5e4 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -1,4 +1,5 @@ <script> +import { __, sprintf } from '~/locale'; import Timeago from 'timeago.js'; import _ from 'underscore'; import { GlTooltipDirective } from '@gitlab/ui'; @@ -14,7 +15,6 @@ import MonitoringButtonComponent from './environment_monitoring.vue'; import CommitComponent from '../../vue_shared/components/commit.vue'; import eventHub from '../event_hub'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; -import { CLUSTER_TYPE } from '~/clusters/constants'; /** * Environment Item Component @@ -80,15 +80,6 @@ export default { }, /** - * Hide group cluster features which are not currently implemented. - * - * @returns {Boolean} - */ - disableGroupClusterFeatures() { - return this.model && this.model.cluster_type === CLUSTER_TYPE.GROUP; - }, - - /** * Returns whether the environment can be stopped. * * @returns {Boolean} @@ -172,7 +163,9 @@ export default { this.model.last_deployment.user && this.model.last_deployment.user.username ) { - return `${this.model.last_deployment.user.username}'s avatar'`; + return sprintf(__("%{username}'s avatar"), { + username: this.model.last_deployment.user.username, + }); } return ''; }, @@ -293,6 +286,9 @@ export default { * @returns {Boolean|Undefined} */ isLastDeployment() { + // TODO: when the vue i18n rules are merged need to disable @gitlab/i18n/no-non-i18n-strings + // name: 'last?' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26#possible-false-positives + // Vue i18n ESLint rules issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/63560 return this.model && this.model.last_deployment && this.model.last_deployment['last?']; }, @@ -575,7 +571,6 @@ export default { <terminal-button-component v-if="model && model.terminal_path" :terminal-path="model.terminal_path" - :disabled="disableGroupClusterFeatures" /> <rollback-component diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue index ae4f07a71cd..886490847ea 100644 --- a/app/assets/javascripts/environments/components/environment_monitoring.vue +++ b/app/assets/javascripts/environments/components/environment_monitoring.vue @@ -1,4 +1,5 @@ <script> +import { __ } from '~/locale'; /** * Renders the Monitoring (Metrics) link in environments table. */ @@ -21,7 +22,7 @@ export default { }, computed: { title() { - return 'Monitoring'; + return __('Monitoring'); }, }, }; diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.vue b/app/assets/javascripts/environments/components/environment_terminal_button.vue index 13195d32cc4..37f94f9f5ab 100644 --- a/app/assets/javascripts/environments/components/environment_terminal_button.vue +++ b/app/assets/javascripts/environments/components/environment_terminal_button.vue @@ -5,6 +5,7 @@ */ import { GlTooltipDirective } from '@gitlab/ui'; import Icon from '~/vue_shared/components/icon.vue'; +import { __ } from '~/locale'; export default { components: { @@ -27,7 +28,7 @@ export default { }, computed: { title() { - return 'Terminal'; + return __('Terminal'); }, }, }; diff --git a/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue b/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue index 060d8e25227..ef1d1e49320 100644 --- a/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue +++ b/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue @@ -49,9 +49,9 @@ export default { </p> </div> <div class="form-group" :class="{ 'gl-show-field-errors': connectError }"> - <label class="label-bold" for="error-tracking-token">{{ - s__('ErrorTracking|Auth Token') - }}</label> + <label class="label-bold" for="error-tracking-token"> + {{ s__('ErrorTracking|Auth Token') }} + </label> <div class="row"> <div class="col-8 col-md-9 gl-pr-0"> <gl-form-input @@ -65,9 +65,8 @@ export default { <gl-button class="js-error-tracking-connect prepend-left-5" @click="$emit('handle-connect')" + >{{ __('Connect') }}</gl-button > - {{ __('Connect') }} - </gl-button> <icon v-show="connectSuccessful" class="js-error-tracking-connect-success prepend-left-5 text-success align-middle" 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/filtered_search/components/recent_searches_dropdown_content.vue b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue index 19bc3313373..4757c4b1e43 100644 --- a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue +++ b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue @@ -59,7 +59,7 @@ export default { <template> <div> <div v-if="!isLocalStorageAvailable" class="dropdown-info-note"> - This feature requires local storage to be enabled + {{ __('This feature requires local storage to be enabled') }} </div> <ul v-else-if="hasItems"> <li v-for="(item, index) in processedItems" :key="`processed-items-${index}`"> @@ -90,10 +90,10 @@ export default { class="filtered-search-history-clear-button" @click="onRequestClearRecentSearches($event)" > - Clear recent searches + {{ __('Clear recent searches') }} </button> </li> </ul> - <div v-else class="dropdown-info-note">You don't have any recent searches</div> + <div v-else class="dropdown-info-note">{{ __("You don't have any recent searches") }}</div> </div> </template> diff --git a/app/assets/javascripts/group.js b/app/assets/javascripts/group.js index 903c838e266..460174caf4d 100644 --- a/app/assets/javascripts/group.js +++ b/app/assets/javascripts/group.js @@ -1,5 +1,5 @@ import $ from 'jquery'; -import { slugifyWithHyphens } from './lib/utils/text_utility'; +import { slugify } from './lib/utils/text_utility'; export default class Group { constructor() { @@ -14,7 +14,7 @@ export default class Group { } update() { - const slug = slugifyWithHyphens(this.groupName.val()); + const slug = slugify(this.groupName.val()); this.groupPath.val(slug); } diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue index 4be4b02ac1e..c8fbc3cb9f1 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue @@ -107,7 +107,8 @@ export default { @click="openFileInEditor" > <span class="multi-file-commit-list-file-path d-flex align-items-center"> - <file-icon :file-name="file.name" class="append-right-8" />{{ file.name }} + <file-icon :file-name="file.name" class="append-right-8" /> + {{ file.name }} </span> <div class="ml-auto d-flex align-items-center"> <div class="d-flex align-items-center ide-commit-list-changed-icon"> diff --git a/app/assets/javascripts/ide/components/external_link.vue b/app/assets/javascripts/ide/components/external_link.vue index 954f84cea17..d1857f0176a 100644 --- a/app/assets/javascripts/ide/components/external_link.vue +++ b/app/assets/javascripts/ide/components/external_link.vue @@ -27,7 +27,7 @@ export default { target="_blank" rel="noopener noreferrer" > - <span class="vertical-align-middle">Open in file view</span> + <span class="vertical-align-middle">{{ __('Open in file view') }}</span> <icon :size="16" name="external-link" css-classes="vertical-align-middle space-right" /> </a> </div> diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index f952b1e7b80..03756a634d5 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -8,6 +8,7 @@ import { activityBarViews, viewerTypes } from '../constants'; import Editor from '../lib/editor'; import ExternalLink from './external_link.vue'; import FileTemplatesBar from './file_templates/bar.vue'; +import { __ } from '~/locale'; export default { components: { @@ -143,7 +144,9 @@ export default { 'triggerFilesChange', ]), initEditor() { - if (this.shouldHideEditor) return; + if (this.shouldHideEditor && (this.file.content || this.file.raw)) { + return; + } this.editor.clearEditor(); @@ -160,7 +163,14 @@ export default { this.createEditorInstance(); }) .catch(err => { - flash('Error setting up editor. Please try again.', 'alert', document, null, false, true); + flash( + __('Error setting up editor. Please try again.'), + 'alert', + document, + null, + false, + true, + ); throw err; }); }, @@ -247,12 +257,8 @@ export default { role="button" @click.prevent="setFileViewMode({ file, viewMode: 'editor' })" > - <template v-if="viewer === $options.viewerTypes.edit"> - {{ __('Edit') }} - </template> - <template v-else> - {{ __('Review') }} - </template> + <template v-if="viewer === $options.viewerTypes.edit">{{ __('Edit') }}</template> + <template v-else>{{ __('Review') }}</template> </a> </li> <li v-if="file.previewMode" :class="previewTabCSS"> @@ -260,9 +266,8 @@ export default { href="javascript:void(0);" role="button" @click.prevent="setFileViewMode({ file, viewMode: 'preview' })" + >{{ file.previewMode.previewTitle }}</a > - {{ file.previewMode.previewTitle }} - </a> </li> </ul> <external-link :file="file" /> diff --git a/app/assets/javascripts/ide/components/repo_file_status_icon.vue b/app/assets/javascripts/ide/components/repo_file_status_icon.vue index a964d90b090..84a962bfc7d 100644 --- a/app/assets/javascripts/ide/components/repo_file_status_icon.vue +++ b/app/assets/javascripts/ide/components/repo_file_status_icon.vue @@ -1,4 +1,5 @@ <script> +import { __, sprintf } from '~/locale'; import icon from '~/vue_shared/components/icon.vue'; import tooltip from '~/vue_shared/directives/tooltip'; import '~/lib/utils/datetime_utility'; @@ -18,7 +19,9 @@ export default { }, computed: { lockTooltip() { - return `Locked by ${this.file.file_lock.user.name}`; + return sprintf(__(`Locked by %{fileLockUserName}`), { + fileLockUserName: this.file.file_lock.user.name, + }); }, }, }; diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue index f6aa2295844..7615cfc966e 100644 --- a/app/assets/javascripts/ide/components/repo_tab.vue +++ b/app/assets/javascripts/ide/components/repo_tab.vue @@ -1,4 +1,5 @@ <script> +import { __, sprintf } from '~/locale'; import { mapActions } from 'vuex'; import FileIcon from '~/vue_shared/components/file_icon.vue'; @@ -27,9 +28,9 @@ export default { computed: { closeLabel() { if (this.fileHasChanged) { - return `${this.tab.name} changed`; + return sprintf(__(`%{tabname} changed`), { tabname: this.tab.name }); } - return `Close ${this.tab.name}`; + return sprintf(__(`Close %{tabname}`, { tabname: this.tab.name })); }, showChangedIcon() { if (this.tab.pending) return true; 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/actions.js b/app/assets/javascripts/ide/stores/actions.js index 507dc363529..8c0119a1fed 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -62,7 +62,7 @@ export const createTempEntry = ( new Promise(resolve => { const fullName = name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name; - if (state.entries[name]) { + if (state.entries[name] && !state.entries[name].deleted) { flash( `The name "${name.split('/').pop()}" is already taken in this directory.`, 'alert', @@ -208,6 +208,7 @@ export const deleteEntry = ({ commit, dispatch, state }, path) => { } commit(types.DELETE_ENTRY, path); + dispatch('stageChange', path); dispatch('triggerFilesChange'); }; diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js index 01ca6a6b12f..ac34491c1ad 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/actions.js +++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js @@ -186,6 +186,8 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo commit(rootTypes.CLEAR_STAGED_CHANGES, null, { root: true }); + commit(rootTypes.CLEAR_REPLACED_FILES, null, { root: true }); + setTimeout(() => { commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true }); }, 5000); diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js index 86ab76136df..f021729c451 100644 --- a/app/assets/javascripts/ide/stores/mutation_types.js +++ b/app/assets/javascripts/ide/stores/mutation_types.js @@ -60,6 +60,8 @@ export const CLEAR_STAGED_CHANGES = 'CLEAR_STAGED_CHANGES'; export const STAGE_CHANGE = 'STAGE_CHANGE'; export const UNSTAGE_CHANGE = 'UNSTAGE_CHANGE'; +export const CLEAR_REPLACED_FILES = 'CLEAR_REPLACED_FILES'; + export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT'; export const ADD_PENDING_TAB = 'ADD_PENDING_TAB'; export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB'; diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js index ec4c2fdcde2..ea125214ebb 100644 --- a/app/assets/javascripts/ide/stores/mutations.js +++ b/app/assets/javascripts/ide/stores/mutations.js @@ -56,6 +56,11 @@ export default { stagedFiles: [], }); }, + [types.CLEAR_REPLACED_FILES](state) { + Object.assign(state, { + replacedFiles: [], + }); + }, [types.SET_ENTRIES](state, entries) { Object.assign(state, { entries, @@ -70,6 +75,13 @@ export default { Object.assign(state.entries, { [key]: entry, }); + } else if (foundEntry.deleted) { + Object.assign(state.entries, { + [key]: { + ...entry, + replaces: true, + }, + }); } else { const tree = entry.tree.filter( f => foundEntry.tree.find(e => e.path === f.path) === undefined, @@ -144,6 +156,7 @@ export default { raw: file.content, changed: Boolean(changedFile), staged: false, + replaces: false, prevPath: '', moved: false, lastCommitSha: lastCommit.commit.id, diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js index 6ca246c1d63..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 }) { @@ -170,12 +170,16 @@ export default { entries: Object.assign(state.entries, { [path]: Object.assign(state.entries[path], { staged: true, - changed: false, }), }), }); if (stagedFile) { + Object.assign(state, { + replacedFiles: state.replacedFiles.concat({ + ...stagedFile, + }), + }); Object.assign(stagedFile, { ...state.entries[path], }); diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js index d400b9831a9..c4da482bf0a 100644 --- a/app/assets/javascripts/ide/stores/state.js +++ b/app/assets/javascripts/ide/stores/state.js @@ -6,6 +6,7 @@ export default () => ({ currentMergeRequestId: '', changedFiles: [], stagedFiles: [], + replacedFiles: [], endpoints: {}, lastCommitMsg: '', lastCommitPath: '', diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js index fb132c1afc1..01f78a29cf6 100644 --- a/app/assets/javascripts/ide/stores/utils.js +++ b/app/assets/javascripts/ide/stores/utils.js @@ -18,6 +18,7 @@ export const dataStructure = () => ({ active: false, changed: false, staged: false, + replaces: false, lastCommitPath: '', lastCommitSha: '', lastCommit: { @@ -119,7 +120,7 @@ export const commitActionForFile = file => { return commitActionTypes.move; } else if (file.deleted) { return commitActionTypes.delete; - } else if (file.tempFile) { + } else if (file.tempFile && !file.replaces) { return commitActionTypes.create; } @@ -151,7 +152,8 @@ export const createCommitPayload = ({ previous_path: f.prevPath === '' ? undefined : f.prevPath, content: f.prevPath ? null : f.content || undefined, encoding: f.base64 ? 'base64' : 'text', - last_commit_id: newBranch || f.deleted || f.prevPath ? undefined : f.lastCommitSha, + last_commit_id: + newBranch || f.deleted || f.prevPath || f.replaces ? undefined : f.lastCommitSha, })), start_sha: newBranch ? rootGetters.lastCommit.short_id : undefined, }); 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 @@ -727,14 +727,6 @@ export const NavigationType = { }; /** - * 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/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index cc1d85fd97d..d38f59b5861 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -44,11 +44,18 @@ export const pluralize = (str, count) => str + (count > 1 || count === 0 ? 's' : export const dasherize = str => str.replace(/[_\s]+/g, '-'); /** - * Replaces whitespaces with hyphens and converts to lower case + * Replaces whitespaces with hyphens, convert to lower case and remove non-allowed special characters * @param {String} str * @returns {String} */ -export const slugifyWithHyphens = str => str.toLowerCase().replace(/\s+/g, '-'); +export const slugify = str => { + const slug = str + .trim() + .toLowerCase() + .replace(/[^a-zA-Z0-9_.-]+/g, '-'); + + return slug === '-' ? '' : slug; +}; /** * Replaces whitespaces with underscore and converts to lower case 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/manual_ordering.js b/app/assets/javascripts/manual_ordering.js index e16ddbfef7e..012d1e70410 100644 --- a/app/assets/javascripts/manual_ordering.js +++ b/app/assets/javascripts/manual_ordering.js @@ -21,7 +21,7 @@ const updateIssue = (url, issueList, { move_before_id, move_after_id }) => const initManualOrdering = () => { const issueList = document.querySelector('.manual-ordering'); - if (!issueList || !(gon.features && gon.features.manualSorting)) { + if (!issueList || !(gon.features && gon.features.manualSorting) || !(gon.current_user_id > 0)) { return; } 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 { </div> </div> <div class="d-flex"> - <div v-if="isEE && canAddMetrics && !showEmptyState"> + <div v-if="addingMetricsAvailable"> <gl-button v-gl-modal-directive="$options.addMetric.modalId" class="js-add-metric-button text-success border-success" @@ -367,7 +377,7 @@ export default { group-id="monitor-area-chart" > <alert-widget - v-if="isEE && prometheusAlertsAvailable && alertsEndpoint && graphData" + v-if="alertWidgetAvailable && graphData" :alerts-endpoint="alertsEndpoint" :relevant-queries="graphData.queries" :alerts-to-manage="getGraphAlerts(graphData.queries)" @@ -386,6 +396,7 @@ export default { :empty-loading-svg-path="emptyLoadingSvgPath" :empty-no-data-svg-path="emptyNoDataSvgPath" :empty-unable-to-connect-svg-path="emptyUnableToConnectSvgPath" + :compact="smallEmptyState" /> </div> </template> 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 @@ <script> import { __ } from '~/locale'; +import { GlEmptyState } from '@gitlab/ui'; export default { + components: { + GlEmptyState, + }, props: { documentationPath: { type: String, @@ -37,6 +41,11 @@ export default { type: String, required: true, }, + compact: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -58,6 +67,8 @@ export default { If this takes a long time, ensure that data is available.`), buttonText: __('View documentation'), buttonPath: this.documentationPath, + secondaryButtonText: '', + secondaryButtonPath: '', }, noData: { svgUrl: this.emptyNoDataSvgPath, @@ -66,13 +77,19 @@ export default { no data to display.`), buttonText: __('Configure Prometheus'), buttonPath: this.settingsPath, + secondaryButtonText: '', + secondaryButtonPath: '', }, unableToConnect: { svgUrl: this.emptyUnableToConnectSvgPath, title: __('Unable to connect to Prometheus server'), - description: 'Ensure connectivity is available from the GitLab server to the ', + description: __( + 'Ensure connectivity is available from the GitLab server to the Prometheus server', + ), buttonText: __('View documentation'), buttonPath: this.documentationPath, + secondaryButtonText: __('Configure Prometheus'), + secondaryButtonPath: this.settingsPath, }, }, }; @@ -81,45 +98,19 @@ export default { currentState() { return this.states[this.selectedState]; }, - showButtonDescription() { - if (this.selectedState === 'unableToConnect') return true; - return false; - }, }, }; </script> <template> - <div class="row empty-state js-empty-state"> - <div class="col-12"> - <div class="state-svg svg-content"> - <img :src="currentState.svgUrl" /> - </div> - </div> - - <div class="col-12"> - <div class="text-content"> - <h4 class="state-title text-center">{{ currentState.title }}</h4> - <p class="state-description"> - {{ currentState.description }} - <a v-if="showButtonDescription" :href="settingsPath">{{ __('Prometheus server') }}</a> - </p> - - <div class="text-center"> - <a - v-if="currentState.buttonPath" - :href="currentState.buttonPath" - class="btn btn-success" - >{{ currentState.buttonText }}</a - > - <a - v-if="currentState.secondaryButtonPath" - :href="currentState.secondaryButtonPath" - class="btn" - >{{ currentState.secondaryButtonText }}</a - > - </div> - </div> - </div> - </div> + <gl-empty-state + :title="currentState.title" + :description="currentState.description" + :primary-button-text="currentState.buttonText" + :primary-button-link="currentState.buttonPath" + :secondary-button-text="currentState.secondaryButtonText" + :secondary-button-link="currentState.secondaryButtonPath" + :svg-path="currentState.svgUrl" + :compact="compact" + /> </template> 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 075c28e8d07..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 discussion'; + 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 }} </button> <button :disabled="isSubmitButtonDisabled" @@ -390,7 +396,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" class="btn btn-success note-type-toggle js-note-new-discussion dropdown-toggle qa-note-dropdown" data-display="static" data-toggle="dropdown" - aria-label="Open comment type dropdown" + :aria-label="__('Open comment type dropdown')" > <i aria-hidden="true" class="fa fa-caret-down toggle-icon"> </i> </button> @@ -404,8 +410,14 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" > <i aria-hidden="true" class="fa fa-check icon"> </i> <div class="description"> - <strong>Comment</strong> - <p>Add a general comment to this {{ noteableDisplayName }}.</p> + <strong>{{ __('Comment') }}</strong> + <p> + {{ + sprintf(__('Add a general comment to this %{noteableDisplayName}.'), { + noteableDisplayName, + }) + }} + </p> </div> </button> </li> @@ -418,7 +430,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" > <i aria-hidden="true" class="fa fa-check icon"> </i> <div class="description"> - <strong>Start discussion</strong> + <strong>{{ __('Start thread') }}</strong> <p>{{ startDiscussionDescription }}</p> </div> </button> 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') }} </button> </td> <td v-else class="line_content js-success-lazy-load"> diff --git a/app/assets/javascripts/notes/components/discussion_actions.vue b/app/assets/javascripts/notes/components/discussion_actions.vue index 1357a5268d6..f4570c1292c 100644 --- a/app/assets/javascripts/notes/components/discussion_actions.vue +++ b/app/assets/javascripts/notes/components/discussion_actions.vue @@ -39,15 +39,23 @@ export default { </script> <template> - <div class="discussion-with-resolve-btn clearfix"> - <reply-placeholder class="qa-discussion-reply" @onClick="$emit('showReplyForm')" /> - - <div class="btn-group discussion-actions" role="group"> - <resolve-discussion-button - v-if="discussion.resolvable" - :is-resolving="isResolving" - :button-title="resolveButtonTitle" - @onClick="$emit('resolve')" + <div class="discussion-with-resolve-btn"> + <reply-placeholder + :button-text="s__('MergeRequests|Reply...')" + class="qa-discussion-reply" + @onClick="$emit('showReplyForm')" + /> + <resolve-discussion-button + v-if="discussion.resolvable" + :is-resolving="isResolving" + :button-title="resolveButtonTitle" + @onClick="$emit('resolve')" + /> + <div v-if="discussion.resolvable" class="btn-group discussion-actions ml-sm-2" role="group"> + <resolve-with-issue-button v-if="resolveWithIssuePath" :url="resolveWithIssuePath" /> + <jump-to-next-discussion-button + v-if="shouldShowJumpToNextDiscussion" + @onClick="$emit('jumpToNextDiscussion')" /> <resolve-with-issue-button v-if="discussion.resolvable && resolveWithIssuePath" diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue index efd84f5722c..d7ffa0abb79 100644 --- a/app/assets/javascripts/notes/components/discussion_counter.vue +++ b/app/assets/javascripts/notes/components/discussion_counter.vue @@ -61,7 +61,7 @@ export default { </span> <span class="line-resolve-text"> {{ resolvedDiscussionsCount }}/{{ resolvableDiscussionsCount }} - {{ n__('discussion resolved', 'discussions resolved', resolvableDiscussionsCount) }} + {{ n__('thread resolved', 'threads resolved', resolvableDiscussionsCount) }} </span> </div> <div @@ -72,7 +72,7 @@ export default { <a v-gl-tooltip :href="resolveAllDiscussionsIssuePath" - :title="s__('Resolve all discussions in new issue')" + :title="s__('Resolve all threads in new issue')" class="new-issue-for-discussion btn btn-default discussion-create-issue-btn" > <icon name="issue-new" /> @@ -81,7 +81,7 @@ export default { <div v-if="isLoggedIn && !allResolved" class="btn-group btn-group-sm" role="group"> <button v-gl-tooltip - title="Jump to first unresolved discussion" + title="Jump to first unresolved thread" class="btn btn-default discussion-next-btn" @click="jumpToFirstUnresolvedDiscussion" > diff --git a/app/assets/javascripts/notes/components/discussion_notes.vue b/app/assets/javascripts/notes/components/discussion_notes.vue index 30971ad5227..0b136549c14 100644 --- a/app/assets/javascripts/notes/components/discussion_notes.vue +++ b/app/assets/javascripts/notes/components/discussion_notes.vue @@ -1,19 +1,21 @@ <script> -import { mapGetters } from 'vuex'; +import { mapGetters, mapActions } from 'vuex'; import { SYSTEM_NOTE } from '../constants'; import { __ } from '~/locale'; -import NoteableNote from './noteable_note.vue'; -import PlaceholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; -import PlaceholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; +import PlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue'; +import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue'; 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: { @@ -72,6 +74,7 @@ export default { }, }, methods: { + ...mapActions(['toggleDiscussion']), componentName(note) { if (note.isPlaceholderNote) { if (note.placeholderType === SYSTEM_NOTE) { @@ -101,7 +104,7 @@ export default { <component :is="componentName(firstNote)" :note="componentData(firstNote)" - :line="line" + :line="line || diffLine" :commit="commit" :help-page-path="helpPagePath" :show-reply-button="userCanReply" @@ -118,23 +121,27 @@ export default { /> <slot slot="avatar-badge" name="avatar-badge"></slot> </component> - <toggle-replies-widget - v-if="hasReplies" - :collapsed="!isExpanded" - :replies="replies" - @toggle="$emit('toggleDiscussion')" - /> - <template v-if="isExpanded"> - <component - :is="componentName(note)" - v-for="note in replies" - :key="note.id" - :note="componentData(note)" - :help-page-path="helpPagePath" - :line="line" - @handleDeleteNote="$emit('deleteNote')" + <discussion-notes-replies-wrapper :is-diff-discussion="discussion.diff_discussion"> + <toggle-replies-widget + v-if="hasReplies" + :collapsed="!isExpanded" + :replies="replies" + :class="{ 'discussion-toggle-replies': discussion.diff_discussion }" + @toggle="toggleDiscussion({ discussionId: discussion.id })" /> - </template> + <template v-if="isExpanded"> + <component + :is="componentName(note)" + v-for="note in replies" + :key="note.id" + :note="componentData(note)" + :help-page-path="helpPagePath" + :line="line" + @handleDeleteNote="$emit('deleteNote')" + /> + </template> + <slot :show-replies="isExpanded || !hasReplies" name="footer"></slot> + </discussion-notes-replies-wrapper> </template> <template v-else> <component @@ -148,8 +155,8 @@ export default { > <slot v-if="index === 0" slot="avatar-badge" name="avatar-badge"></slot> </component> + <slot :show-replies="isExpanded || !hasReplies" name="footer"></slot> </template> </ul> - <slot :show-replies="isExpanded || !hasReplies" name="footer"></slot> </div> </template> diff --git a/app/assets/javascripts/notes/components/discussion_notes_replies_wrapper.vue b/app/assets/javascripts/notes/components/discussion_notes_replies_wrapper.vue new file mode 100644 index 00000000000..2ddca56ddd5 --- /dev/null +++ b/app/assets/javascripts/notes/components/discussion_notes_replies_wrapper.vue @@ -0,0 +1,27 @@ +<script> +/** + * Wrapper for discussion notes replies section. + * + * This is a functional component using the render method because in some cases + * the wrapper is not needed and we want to simply render along the children. + */ +export default { + functional: true, + props: { + isDiffDiscussion: { + type: Boolean, + required: false, + default: false, + }, + }, + render(h, { props, children }) { + if (props.isDiffDiscussion) { + return h('li', { class: 'discussion-collapsible bordered-box clearfix' }, [ + h('ul', { class: 'notes' }, children), + ]); + } + + return children; + }, +}; +</script> diff --git a/app/assets/javascripts/notes/components/discussion_reply_placeholder.vue b/app/assets/javascripts/notes/components/discussion_reply_placeholder.vue index ea590905e3c..0204169214b 100644 --- a/app/assets/javascripts/notes/components/discussion_reply_placeholder.vue +++ b/app/assets/javascripts/notes/components/discussion_reply_placeholder.vue @@ -1,6 +1,12 @@ <script> export default { name: 'ReplyPlaceholder', + props: { + buttonText: { + type: String, + required: true, + }, + }, }; </script> @@ -12,6 +18,6 @@ export default { :title="s__('MergeRequests|Add a reply')" @click="$emit('onClick')" > - {{ s__('MergeRequests|Reply...') }} + {{ buttonText }} </button> </template> diff --git a/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue b/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue index e413398696a..f03e6fd73d7 100644 --- a/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue +++ b/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue @@ -25,7 +25,7 @@ export default { <gl-button v-gl-tooltip :href="url" - :title="s__('MergeRequests|Resolve this discussion in a new issue')" + :title="s__('MergeRequests|Resolve this thread in a new issue')" class="new-issue-for-discussion discussion-create-issue-btn" > <icon name="issue-new" /> diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue index 941b6d5cab3..d4a57d5d58d 100644 --- a/app/assets/javascripts/notes/components/note_awards_list.vue +++ b/app/assets/javascripts/notes/components/note_awards_list.vue @@ -4,6 +4,7 @@ import tooltip from '~/vue_shared/directives/tooltip'; import Icon from '~/vue_shared/components/icon.vue'; import Flash from '../../flash'; import { glEmojiTag } from '../../emoji'; +import { __, sprintf } from '~/locale'; export default { components: { @@ -108,23 +109,26 @@ export default { // Add myself to the beginning of the list so title will start with You. if (hasReactionByCurrentUser) { - namesToShow.unshift('You'); + namesToShow.unshift(__('You')); } let title = ''; // We have 10+ awarded user, join them with comma and add `and x more`. if (remainingAwardList.length) { - title = `${namesToShow.join(', ')}, and ${remainingAwardList.length} more.`; + title = sprintf(__(`%{listToShow}, and %{awardsListLength} more.`), { + listToShow: namesToShow.join(', '), + awardsListLength: remainingAwardList.length, + }); } else if (namesToShow.length > 1) { // Join all names with comma but not the last one, it will be added with and text. title = namesToShow.slice(0, namesToShow.length - 1).join(', '); // If we have more than 2 users we need an extra comma before and text. title += namesToShow.length > 2 ? ',' : ''; - title += ` and ${namesToShow.slice(-1)}`; // Append and text + title += sprintf(__(` and %{sliced}`), { sliced: namesToShow.slice(-1) }); // Append and text } else { // We have only 2 users so join them with and. - title = namesToShow.join(' and '); + title = namesToShow.join(__(' and ')); } return title; @@ -155,7 +159,7 @@ export default { awardName: parsedName, }; - this.toggleAwardRequest(data).catch(() => Flash('Something went wrong on our end.')); + this.toggleAwardRequest(data).catch(() => Flash(__('Something went wrong on our end.'))); }, }, }; @@ -184,7 +188,7 @@ export default { :class="{ 'js-user-authored': isAuthoredByMe }" class="award-control btn js-add-award" title="Add reaction" - aria-label="Add reaction" + :aria-label="__('Add reaction')" data-boundary="viewport" type="button" > diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 042ed196933..3823861c0b9 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -1,14 +1,14 @@ <script> import { mergeUrlParams } from '~/lib/utils/url_utility'; import { mapGetters, mapActions } from 'vuex'; +import noteFormMixin from 'ee_else_ce/notes/mixins/note_form'; import eventHub from '../event_hub'; import issueWarning from '../../vue_shared/components/issue/issue_warning.vue'; import markdownField from '../../vue_shared/components/markdown/field.vue'; import issuableStateMixin from '../mixins/issuable_state'; import resolvable from '../mixins/resolvable'; -import { __ } from '~/locale'; +import { __, sprintf } from '~/locale'; import { getDraft, updateDraft } from '~/lib/utils/autosave'; -import noteFormMixin from 'ee_else_ce/notes/mixins/note_form'; export default { name: 'NoteForm', @@ -174,6 +174,18 @@ export default { (this.line && this.line.can_receive_suggestion) ); }, + changedCommentText() { + return sprintf( + __( + 'This comment has changed since you started editing, please review the %{startTag}updated comment%{endTag} to ensure information is not lost.', + ), + { + startTag: `<a href="${this.noteHash}" target="_blank" rel="noopener noreferrer">`, + endTag: '</a>', + }, + false, + ); + }, }, watch: { noteBody() { @@ -228,11 +240,11 @@ export default { <template> <div ref="editNoteForm" class="note-edit-form current-note-edit-form js-discussion-note-form"> - <div v-if="conflictWhileEditing" class="js-conflict-edit-warning alert alert-danger"> - This comment has changed since you started editing, please review the - <a :href="noteHash" target="_blank" rel="noopener noreferrer">updated comment</a> to ensure - information is not lost. - </div> + <div + v-if="conflictWhileEditing" + class="js-conflict-edit-warning alert alert-danger" + v-html="changedCommentText" + ></div> <div class="flash-container timeline-content"></div> <form :data-line-code="lineCode" class="edit-note common-note-form js-quick-submit gfm-form"> <issue-warning @@ -264,8 +276,8 @@ export default { name="note[note]" class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form js-vue-textarea qa-reply-input" dir="auto" - 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.meta.enter="handleKeySubmit()" @keydown.ctrl.enter="handleKeySubmit()" @keydown.exact.up="editMyLastNote()" @@ -283,11 +295,11 @@ export default { type="checkbox" class="qa-unresolve-review-discussion" /> - {{ __('Unresolve discussion') }} + {{ __('Unresolve thread') }} </template> <template v-else> <input v-model="isResolving" type="checkbox" class="qa-resolve-review-discussion" /> - {{ __('Resolve discussion') }} + {{ __('Resolve thread') }} </template> </label> </p> @@ -339,7 +351,7 @@ export default { type="button" @click="cancelHandler()" > - Cancel + {{ __('Cancel') }} </button> </template> </div> diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue index fbf82fab9e9..3158e086f6c 100644 --- a/app/assets/javascripts/notes/components/note_header.vue +++ b/app/assets/javascripts/notes/components/note_header.vue @@ -70,7 +70,7 @@ export default { @click="handleToggle" > <i :class="toggleChevronClass" class="fa" aria-hidden="true"></i> - {{ __('Toggle discussion') }} + {{ __('Toggle thread') }} </button> </div> <a @@ -103,7 +103,7 @@ export default { </template> <i class="fa fa-spinner fa-spin editing-spinner" - aria-label="Comment is being updated" + :aria-label="__('Comment is being updated')" aria-hidden="true" ></i> </span> diff --git a/app/assets/javascripts/notes/components/note_signed_out_widget.vue b/app/assets/javascripts/notes/components/note_signed_out_widget.vue index e3eb92956b1..ccfe84ab098 100644 --- a/app/assets/javascripts/notes/components/note_signed_out_widget.vue +++ b/app/assets/javascripts/notes/components/note_signed_out_widget.vue @@ -1,5 +1,6 @@ <script> import { mapGetters } from 'vuex'; +import { __, sprintf } from '~/locale'; export default { computed: { @@ -10,12 +11,24 @@ export default { signInLink() { return this.getNotesDataByProp('newSessionPath'); }, + signedOutText() { + return sprintf( + __( + 'Please %{startTagRegister}register%{endRegisterTag} or %{startTagSignIn}sign in%{endSignInTag} to reply', + ), + { + startTagRegister: `<a href="${this.registerLink}">`, + startTagSignIn: `<a href="${this.signInLink}">`, + endRegisterTag: '</a>', + endSignInTag: '</a>', + }, + false, + ); + }, }, }; </script> <template> - <div class="disabled-comment text-center"> - Please <a :href="registerLink">register</a> or <a :href="signInLink">sign in</a> to reply - </div> + <div class="disabled-comment text-center" v-html="signedOutText"></div> </template> diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index b8eaff32cce..ac743d9f4b8 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -132,7 +132,7 @@ export default { return this.discussion.diff_discussion && this.renderDiffFile; }, shouldGroupReplies() { - return !this.shouldRenderDiffs && !this.discussion.diff_discussion; + return !this.shouldRenderDiffs; }, wrapperComponent() { return this.shouldRenderDiffs ? diffWithNote : 'div'; @@ -144,15 +144,6 @@ export default { return {}; }, - componentClassName() { - if (this.shouldRenderDiffs) { - if (!this.lastUpdatedAt && !this.discussion.resolved) { - return 'unresolved'; - } - } - - return ''; - }, isExpanded() { return this.discussion.expanded || this.alwaysExpanded; }, @@ -174,22 +165,20 @@ export default { active: isActive, } = this.discussion; - let text = s__('MergeRequests|started a discussion'); + let text = s__('MergeRequests|started a thread'); if (isForCommit) { - text = s__( - 'MergeRequests|started a discussion on commit %{linkStart}%{commitId}%{linkEnd}', - ); + text = s__('MergeRequests|started a thread on commit %{linkStart}%{commitId}%{linkEnd}'); } else if (isDiffDiscussion && commitId) { text = isActive - ? s__('MergeRequests|started a discussion on commit %{linkStart}%{commitId}%{linkEnd}') + ? s__('MergeRequests|started a thread on commit %{linkStart}%{commitId}%{linkEnd}') : s__( - 'MergeRequests|started a discussion on an outdated change in commit %{linkStart}%{commitId}%{linkEnd}', + 'MergeRequests|started a thread on an outdated change in commit %{linkStart}%{commitId}%{linkEnd}', ); } else if (isDiffDiscussion) { text = isActive - ? s__('MergeRequests|started a discussion on %{linkStart}the diff%{linkEnd}') + ? s__('MergeRequests|started a thread on %{linkStart}the diff%{linkEnd}') : s__( - 'MergeRequests|started a discussion on %{linkStart}an old version of the diff%{linkEnd}', + 'MergeRequests|started a thread on %{linkStart}an old version of the diff%{linkEnd}', ); } @@ -250,6 +239,11 @@ export default { clearDraft(this.autosaveKey); }, saveReply(noteText, form, callback) { + if (!noteText) { + this.cancelReplyForm(); + callback(); + return; + } const postData = { in_reply_to_discussion_id: this.discussion.reply_id, target_type: this.getNoteableData.targetType, @@ -280,8 +274,9 @@ export default { this.removePlaceholderNotes(); this.isReplying = true; this.$nextTick(() => { - 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.$refs.noteForm.note = noteText; callback(err); @@ -309,11 +304,11 @@ Please check your network connection and try again.`; </script> <template> - <timeline-entry-item class="note note-discussion" :class="componentClassName"> + <timeline-entry-item class="note note-discussion"> <div class="timeline-content"> <div :data-discussion-id="discussion.id" class="discussion js-discussion-container"> <div v-if="shouldRenderDiffs" class="discussion-header note-wrapper"> - <div v-once class="timeline-icon"> + <div v-once class="timeline-icon align-self-start flex-shrink-0"> <user-avatar-link v-if="author" :link-href="author.path" @@ -322,7 +317,7 @@ Please check your network connection and try again.`; :img-size="40" /> </div> - <div class="timeline-content"> + <div class="timeline-content w-100"> <note-header :author="author" :created-at="firstNote.created_at" @@ -363,7 +358,6 @@ Please check your network connection and try again.`; :line="line" :should-group-replies="shouldGroupReplies" @startReplying="showReplyForm" - @toggleDiscussion="toggleDiscussionHandler" @deleteNote="deleteNoteHandler" > <slot slot="avatar-badge" name="avatar-badge"></slot> @@ -376,7 +370,7 @@ Please check your network connection and try again.`; <div v-else-if="showReplies" :class="{ 'is-replying': isReplying }" - class="discussion-reply-holder" + class="discussion-reply-holder clearfix" > <user-avatar-link v-if="!isReplying && userCanReply" diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index aa80e25a3e0..2f201839d45 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -5,7 +5,7 @@ import { escape } from 'underscore'; import { truncateSha } from '~/lib/utils/text_utility'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; import draftMixin from 'ee_else_ce/notes/mixins/draft'; -import { s__, sprintf } from '../../locale'; +import { __, s__, sprintf } from '../../locale'; import Flash from '../../flash'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import noteHeader from './note_header.vue'; @@ -128,9 +128,13 @@ export default { this.$emit('handleEdit'); }, deleteHandler() { - const typeOfComment = this.note.isDraft ? 'pending comment' : 'comment'; - // eslint-disable-next-line no-alert - if (window.confirm(`Are you sure you want to delete this ${typeOfComment}?`)) { + const typeOfComment = this.note.isDraft ? __('pending comment') : __('comment'); + if ( + // eslint-disable-next-line no-alert + window.confirm( + sprintf(__('Are you sure you want to delete this %{typeOfComment}?'), { typeOfComment }), + ) + ) { this.isDeleting = true; this.$emit('handleDeleteNote', this.note); @@ -141,7 +145,7 @@ export default { this.isDeleting = false; }) .catch(() => { - Flash('Something went wrong while deleting your note. Please try again.'); + Flash(__('Something went wrong while deleting your note. Please try again.')); this.isDeleting = false; }); } @@ -185,7 +189,7 @@ export default { this.isRequesting = false; this.isEditing = true; this.$nextTick(() => { - const msg = 'Something went wrong while editing your comment. Please try again.'; + const msg = __('Something went wrong while editing your comment. Please try again.'); Flash(msg, 'alert', this.$el); this.recoverNoteContent(noteText); callback(); @@ -195,7 +199,7 @@ export default { formCancelHandler(shouldConfirm, isDirty) { if (shouldConfirm && isDirty) { // eslint-disable-next-line no-alert - if (!window.confirm('Are you sure you want to cancel editing this comment?')) return; + if (!window.confirm(__('Are you sure you want to cancel editing this comment?'))) return; } this.$refs.noteBody.resetAutoSave(); if (this.oldContent) { diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index 4d00e957973..a0695f9e191 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -1,4 +1,5 @@ <script> +import { __ } from '~/locale'; import { mapGetters, mapActions } from 'vuex'; import { getLocationHash } from '../../lib/utils/url_utility'; import Flash from '../../flash'; @@ -170,7 +171,7 @@ export default { .catch(() => { this.setLoadingState(false); this.setNotesFetchedState(true); - Flash('Something went wrong while fetching comments. Please try again.'); + Flash(__('Something went wrong while fetching comments. Please try again.')); }); }, initPolling() { diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js index 57dd1c5cab2..c70c0e4095c 100644 --- a/app/assets/javascripts/notes/index.js +++ b/app/assets/javascripts/notes/index.js @@ -1,5 +1,4 @@ import Vue from 'vue'; -import { isEE } from '~/lib/utils/common_utils'; import initNoteStats from 'ee_else_ce/event_tracking/notes'; import notesApp from './components/notes_app.vue'; import initDiscussionFilters from './discussion_filters'; @@ -41,9 +40,7 @@ document.addEventListener('DOMContentLoaded', () => { }; }, mounted() { - if (isEE) { - initNoteStats(); - } + initNoteStats(); }, render(createElement) { return createElement('notes-app', { diff --git a/app/assets/javascripts/notes/mixins/resolvable.js b/app/assets/javascripts/notes/mixins/resolvable.js index 2329727bca2..16b7598ee09 100644 --- a/app/assets/javascripts/notes/mixins/resolvable.js +++ b/app/assets/javascripts/notes/mixins/resolvable.js @@ -20,13 +20,13 @@ export default { resolveButtonTitle() { if (this.updatedNoteBody) { if (this.discussionResolved) { - return __('Comment & unresolve discussion'); + return __('Comment & unresolve thread'); } - return __('Comment & resolve discussion'); + return __('Comment & resolve thread'); } - return this.discussionResolved ? __('Unresolve discussion') : __('Resolve discussion'); + return this.discussionResolved ? __('Unresolve thread') : __('Resolve thread'); }, }, methods: { diff --git a/app/assets/javascripts/notes/services/notes_service.js b/app/assets/javascripts/notes/services/notes_service.js index 237e70c0a4c..47a6f07cce2 100644 --- a/app/assets/javascripts/notes/services/notes_service.js +++ b/app/assets/javascripts/notes/services/notes_service.js @@ -1,5 +1,4 @@ import Vue from 'vue'; -import Api from '~/api'; import VueResource from 'vue-resource'; import * as constants from '../constants'; @@ -45,7 +44,4 @@ export default { toggleIssueState(endpoint, data) { return Vue.http.put(endpoint, data); }, - applySuggestion(id) { - return Api.applySuggestion(id); - }, }; diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 63658d49a05..fef962f008e 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -14,6 +14,7 @@ import sidebarTimeTrackingEventHub from '../../sidebar/event_hub'; import { isInViewport, scrollToElement, isInMRPage } from '../../lib/utils/common_utils'; import mrWidgetEventHub from '../../vue_merge_request_widget/event_hub'; import { __ } from '~/locale'; +import Api from '~/api'; let eTagPoll; @@ -51,7 +52,7 @@ export const fetchDiscussions = ({ commit, dispatch }, { path, filter }) => .then(res => res.json()) .then(discussions => { commit(types.SET_INITIAL_DISCUSSIONS, discussions); - dispatch('updateResolvableDiscussonsCounts'); + dispatch('updateResolvableDiscussionsCounts'); }); export const updateDiscussion = ({ commit, state }, discussion) => { @@ -67,7 +68,7 @@ export const deleteNote = ({ commit, dispatch, state }, note) => commit(types.DELETE_NOTE, note); dispatch('updateMergeRequestWidget'); - dispatch('updateResolvableDiscussonsCounts'); + dispatch('updateResolvableDiscussionsCounts'); if (isInMRPage()) { dispatch('diffs/removeDiscussionsFromDiff', discussion); @@ -117,7 +118,7 @@ export const replyToDiscussion = ({ commit, state, getters, dispatch }, { endpoi dispatch('updateMergeRequestWidget'); dispatch('startTaskList'); - dispatch('updateResolvableDiscussonsCounts'); + dispatch('updateResolvableDiscussionsCounts'); } else { commit(types.ADD_NEW_REPLY_TO_DISCUSSION, res); } @@ -135,7 +136,7 @@ export const createNewNote = ({ commit, dispatch }, { endpoint, data }) => dispatch('updateMergeRequestWidget'); dispatch('startTaskList'); - dispatch('updateResolvableDiscussonsCounts'); + dispatch('updateResolvableDiscussionsCounts'); } return res; }); @@ -168,7 +169,7 @@ export const toggleResolveNote = ({ commit, dispatch }, { endpoint, isResolved, commit(mutationType, res); - dispatch('updateResolvableDiscussonsCounts'); + dispatch('updateResolvableDiscussionsCounts'); dispatch('updateMergeRequestWidget'); }); @@ -442,15 +443,14 @@ export const startTaskList = ({ dispatch }) => }), ); -export const updateResolvableDiscussonsCounts = ({ commit }) => +export const updateResolvableDiscussionsCounts = ({ commit }) => commit(types.UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS); export const submitSuggestion = ( { commit, dispatch }, { discussionId, noteId, suggestionId, flashContainer }, ) => - service - .applySuggestion(suggestionId) + Api.applySuggestion(suggestionId) .then(() => commit(types.APPLY_SUGGESTION, { discussionId, noteId, suggestionId })) .then(() => dispatch('resolveDiscussion', { discussionId }).catch(() => {})) .catch(err => { diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js index 1b56b97f751..d51d411f3c6 100644 --- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js +++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js @@ -82,7 +82,7 @@ export default class Todos { }) .catch(() => { this.updateRowState(target, true); - return flash(__('Error updating todo status.')); + return flash(__('Error updating status of to-do item.')); }); } @@ -124,7 +124,7 @@ export default class Todos { this.updateAllState(target, data); this.updateBadges(data); }) - .catch(() => flash(__('Error updating status for all todos.'))); + .catch(() => flash(__('Error updating status for all to-do items.'))); } updateAllState(target, data) { diff --git a/app/assets/javascripts/pages/groups/issues/index.js b/app/assets/javascripts/pages/groups/issues/index.js index 23fb5656008..dcdee77a8ab 100644 --- a/app/assets/javascripts/pages/groups/issues/index.js +++ b/app/assets/javascripts/pages/groups/issues/index.js @@ -1,11 +1,15 @@ import projectSelect from '~/project_select'; import initFilteredSearch from '~/pages/search/init_filtered_search'; +import issuableInitBulkUpdateSidebar from '~/issuable_init_bulk_update_sidebar'; import { FILTERED_SEARCH } from '~/pages/constants'; import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys'; import initManualOrdering from '~/manual_ordering'; +const ISSUE_BULK_UPDATE_PREFIX = 'issue_'; + document.addEventListener('DOMContentLoaded', () => { IssuableFilteredSearchTokenKeys.addExtraTokensForIssues(); + issuableInitBulkUpdateSidebar.init(ISSUE_BULK_UPDATE_PREFIX); initFilteredSearch({ page: FILTERED_SEARCH.ISSUES, diff --git a/app/assets/javascripts/pages/projects/branches/index/index.js b/app/assets/javascripts/pages/projects/branches/index/index.js index 29de3b7806c..37e8c75f299 100644 --- a/app/assets/javascripts/pages/projects/branches/index/index.js +++ b/app/assets/javascripts/pages/projects/branches/index/index.js @@ -5,5 +5,5 @@ import initDiverganceGraph from '~/branches/divergence_graph'; document.addEventListener('DOMContentLoaded', () => { AjaxLoadingSpinner.init(); new DeleteModal(); // eslint-disable-line no-new - initDiverganceGraph(); + initDiverganceGraph(document.querySelector('.js-branch-list').dataset.divergingCountsEndpoint); }); diff --git a/app/assets/javascripts/pages/projects/issues/form.js b/app/assets/javascripts/pages/projects/issues/form.js index 941c4552579..2205a7bafe3 100644 --- a/app/assets/javascripts/pages/projects/issues/form.js +++ b/app/assets/javascripts/pages/projects/issues/form.js @@ -17,7 +17,5 @@ export default () => { new MilestoneSelect(); new IssuableTemplateSelectors(); - if (gon.features.graphql) { - initSuggestions(); - } + initSuggestions(); }; diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue index ff6dadeff7d..533065b2d4d 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue @@ -1,5 +1,6 @@ <script> -import projectFeatureToggle from '../../../../../vue_shared/components/toggle_button.vue'; +import projectFeatureToggle from '~/vue_shared/components/toggle_button.vue'; +import { featureAccessLevelNone } from '../constants'; export default { components: { @@ -43,7 +44,7 @@ export default { if (this.featureEnabled) { return this.options; } - return [[0, 'Enable feature to choose access level']]; + return [featureAccessLevelNone]; }, displaySelectInput() { diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue index 0bcfb740469..b4d24f3aa36 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue @@ -1,11 +1,19 @@ <script> import settingsMixin from 'ee_else_ce/pages/projects/shared/permissions/mixins/settings_pannel_mixin'; +import { __ } from '~/locale'; import projectFeatureSetting from './project_feature_setting.vue'; import projectFeatureToggle from '~/vue_shared/components/toggle_button.vue'; import projectSettingRow from './project_setting_row.vue'; -import { visibilityOptions, visibilityLevelDescriptions } from '../constants'; +import { + visibilityOptions, + visibilityLevelDescriptions, + featureAccessLevelMembers, + featureAccessLevelEveryone, +} from '../constants'; import { toggleHiddenClassBySelector } from '../external'; +const PAGE_FEATURE_ACCESS_LEVEL = __('Everyone'); + export default { components: { projectFeatureSetting, @@ -102,9 +110,9 @@ export default { computed: { featureAccessLevelOptions() { - const options = [[10, 'Only Project Members']]; + const options = [featureAccessLevelMembers]; if (this.visibilityLevel !== visibilityOptions.PRIVATE) { - options.push([20, 'Everyone With Access']); + options.push(featureAccessLevelEveryone); } return options; }, @@ -117,7 +125,7 @@ export default { pagesFeatureAccessLevelOptions() { if (this.visibilityLevel !== visibilityOptions.PUBLIC) { - return this.featureAccessLevelOptions.concat([[30, 'Everyone']]); + return this.featureAccessLevelOptions.concat([[30, PAGE_FEATURE_ACCESS_LEVEL]]); } return this.featureAccessLevelOptions; }, @@ -200,17 +208,17 @@ export default { <option :value="visibilityOptions.PRIVATE" :disabled="!visibilityAllowed(visibilityOptions.PRIVATE)" - >Private</option + >{{ __('Private') }}</option > <option :value="visibilityOptions.INTERNAL" :disabled="!visibilityAllowed(visibilityOptions.INTERNAL)" - >Internal</option + >{{ __('Internal') }}</option > <option :value="visibilityOptions.PUBLIC" :disabled="!visibilityAllowed(visibilityOptions.PUBLIC)" - >Public</option + >{{ __('Public') }}</option > </select> <i aria-hidden="true" data-hidden="true" class="fa fa-chevron-down"></i> diff --git a/app/assets/javascripts/pages/projects/shared/permissions/constants.js b/app/assets/javascripts/pages/projects/shared/permissions/constants.js index ac0dca31c37..73269c6f3ba 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/constants.js +++ b/app/assets/javascripts/pages/projects/shared/permissions/constants.js @@ -15,3 +15,30 @@ export const visibilityLevelDescriptions = { 'The project can be accessed by anyone, regardless of authentication.', ), }; + +const featureAccessLevel = { + NOT_ENABLED: 0, + PROJECT_MEMBERS: 10, + EVERYONE: 20, +}; + +const featureAccessLevelDescriptions = { + [featureAccessLevel.NOT_ENABLED]: __('Enable feature to choose access level'), + [featureAccessLevel.PROJECT_MEMBERS]: __('Only Project Members'), + [featureAccessLevel.EVERYONE]: __('Everyone With Access'), +}; + +export const featureAccessLevelNone = [ + featureAccessLevel.NOT_ENABLED, + featureAccessLevelDescriptions[featureAccessLevel.NOT_ENABLED], +]; + +export const featureAccessLevelMembers = [ + featureAccessLevel.PROJECT_MEMBERS, + featureAccessLevelDescriptions[featureAccessLevel.PROJECT_MEMBERS], +]; + +export const featureAccessLevelEveryone = [ + featureAccessLevel.EVERYONE, + featureAccessLevelDescriptions[featureAccessLevel.EVERYONE], +]; diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue index 185003c306e..015c1527500 100644 --- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue +++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue @@ -4,14 +4,12 @@ import { glEmojiTag } from '~/emoji'; import detailedMetric from './detailed_metric.vue'; import requestSelector from './request_selector.vue'; -import simpleMetric from './simple_metric.vue'; import { s__ } from '~/locale'; export default { components: { detailedMetric, requestSelector, - simpleMetric, }, props: { store: { @@ -43,8 +41,13 @@ export default { details: 'details', keys: ['feature', 'request'], }, + { + metric: 'redis', + header: 'Redis calls', + details: 'details', + keys: ['cmd'], + }, ], - simpleMetrics: ['redis'], data() { return { currentRequestId: '' }; }, @@ -124,12 +127,6 @@ export default { </button> <a v-else :href="profileUrl">{{ s__('PerformanceBar|profile') }}</a> </div> - <simple-metric - v-for="metric in $options.simpleMetrics" - :key="metric" - :current-request="currentRequest" - :metric="metric" - /> <div id="peek-view-gc" class="view"> <span v-if="currentRequest.details" class="bold"> <span title="Invoke Time">{{ currentRequest.details.gc.gc_time }}</span diff --git a/app/assets/javascripts/performance_bar/components/simple_metric.vue b/app/assets/javascripts/performance_bar/components/simple_metric.vue deleted file mode 100644 index 358a57d5bc5..00000000000 --- a/app/assets/javascripts/performance_bar/components/simple_metric.vue +++ /dev/null @@ -1,33 +0,0 @@ -<script> -export default { - props: { - currentRequest: { - type: Object, - required: true, - }, - metric: { - type: String, - required: true, - }, - }, - computed: { - duration() { - return ( - this.currentRequest.details[this.metric] && - this.currentRequest.details[this.metric].duration - ); - }, - calls() { - return ( - this.currentRequest.details[this.metric] && this.currentRequest.details[this.metric].calls - ); - }, - }, -}; -</script> -<template> - <div :id="`peek-view-${metric}`" class="view"> - <span v-if="currentRequest.details" class="bold"> {{ duration }} / {{ calls }} </span> - {{ metric }} - </div> -</template> diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue index b2e365e5cde..39afa87afc3 100644 --- a/app/assets/javascripts/pipelines/components/header_component.vue +++ b/app/assets/javascripts/pipelines/components/header_component.vue @@ -2,6 +2,7 @@ import { GlLoadingIcon } from '@gitlab/ui'; import ciHeader from '../../vue_shared/components/header_ci_component.vue'; import eventHub from '../event_hub'; +import { __ } from '~/locale'; export default { name: 'PipelineHeaderSection', @@ -54,7 +55,7 @@ export default { if (this.pipeline.retry_path) { actions.push({ - label: 'Retry', + label: __('Retry'), path: this.pipeline.retry_path, cssClass: 'js-retry-button btn btn-inverted-secondary', type: 'button', @@ -64,7 +65,7 @@ export default { if (this.pipeline.cancel_path) { actions.push({ - label: 'Cancel running', + label: __('Cancel running'), path: this.pipeline.cancel_path, cssClass: 'js-btn-cancel-pipeline btn btn-danger', type: 'button', diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue index 65a2b61396c..3f021a26ec5 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue @@ -94,9 +94,8 @@ export default { tabindex="0" class="js-pipeline-url-autodevops badge badge-info autodevops-badge" role="button" + >{{ __('Auto DevOps') }}</gl-link > - Auto DevOps - </gl-link> <span v-if="pipeline.flags.stuck" class="js-pipeline-url-stuck badge badge-warning"> {{ __('stuck') }} </span> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue index 03d332cd430..d3ba0c97f6b 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue @@ -44,6 +44,11 @@ export default { cancelingPipeline: null, }; }, + watch: { + pipelines() { + this.cancelingPipeline = null; + }, + }, created() { eventHub.$on('openConfirmationModal', this.setModalData); }, diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index e32e2f785bd..5275de3bc8b 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -241,7 +241,11 @@ export default { return this.cancelingPipeline === this.pipeline.id; }, }, - + watch: { + pipeline() { + this.isRetrying = false; + }, + }, methods: { handleCancelClick() { eventHub.$emit('openConfirmationModal', { diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js index 3cc9d0a3a4e..a6243366375 100644 --- a/app/assets/javascripts/pipelines/mixins/pipelines.js +++ b/app/assets/javascripts/pipelines/mixins/pipelines.js @@ -107,8 +107,8 @@ export default { } // Stop polling this.poll.stop(); - // Update the table - return this.getPipelines().then(() => this.poll.restart()); + // Restarting the poll also makes an initial request + this.poll.restart(); }, fetchPipelines() { if (!this.isMakingRequest) { @@ -153,7 +153,7 @@ export default { postAction(endpoint) { this.service .postAction(endpoint) - .then(() => this.fetchPipelines()) + .then(() => this.updateTable()) .catch(() => Flash(__('An error occurred while making the request.'))); }, }, diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js index ea82ff4e340..9066844f687 100644 --- a/app/assets/javascripts/projects/project_new.js +++ b/app/assets/javascripts/projects/project_new.js @@ -1,6 +1,6 @@ import $ from 'jquery'; import { addSelectOnFocusBehaviour } from '../lib/utils/common_utils'; -import { slugifyWithHyphens } from '../lib/utils/text_utility'; +import { slugify } from '../lib/utils/text_utility'; import { s__ } from '~/locale'; let hasUserDefinedProjectPath = false; @@ -34,7 +34,7 @@ const deriveProjectPathFromUrl = $projectImportUrl => { }; const onProjectNameChange = ($projectNameInput, $projectPathInput) => { - const slug = slugifyWithHyphens($projectNameInput.val()); + const slug = slugify($projectNameInput.val()); $projectPathInput.val(slug); }; diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue index bfc55013a71..03281aa1317 100644 --- a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue +++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue @@ -3,7 +3,7 @@ import Visibility from 'visibilityjs'; import ciIcon from '~/vue_shared/components/ci_icon.vue'; import Poll from '~/lib/utils/poll'; import Flash from '~/flash'; -import { s__, sprintf } from '~/locale'; +import { __, s__, sprintf } from '~/locale'; import tooltip from '~/vue_shared/directives/tooltip'; import { GlLoadingIcon } from '@gitlab/ui'; import CommitPipelineService from '../services/commit_pipeline_service'; @@ -56,7 +56,7 @@ export default { }, errorCallback() { this.ciStatus = { - text: 'not found', + text: __('not found'), icon: 'status_notfound', group: 'notfound', }; diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue index ee973017387..7752723baac 100644 --- a/app/assets/javascripts/registry/components/app.vue +++ b/app/assets/javascripts/registry/components/app.vue @@ -3,22 +3,81 @@ import { mapGetters, mapActions } from 'vuex'; import { GlLoadingIcon } from '@gitlab/ui'; import store from '../stores'; import CollapsibleContainer from './collapsible_container.vue'; +import SvgMessage from './svg_message.vue'; +import { s__, sprintf } from '../../locale'; export default { name: 'RegistryListApp', components: { CollapsibleContainer, GlLoadingIcon, + SvgMessage, }, props: { endpoint: { type: String, required: true, }, + characterError: { + type: Boolean, + required: false, + default: false, + }, + helpPagePath: { + type: String, + required: true, + }, + noContainersImage: { + type: String, + required: true, + }, + containersErrorImage: { + type: String, + required: true, + }, + repositoryUrl: { + type: String, + required: true, + }, }, store, computed: { ...mapGetters(['isLoading', 'repos']), + dockerConnectionErrorText() { + return sprintf( + s__(`ContainerRegistry|We are having trouble connecting to Docker, which could be due to an + issue with your project name or path. For more information, please review the + %{docLinkStart}Container Registry documentation%{docLinkEnd}.`), + { + docLinkStart: `<a href="${this.helpPagePath}#docker-connection-error">`, + docLinkEnd: '</a>', + }, + false, + ); + }, + introText() { + return sprintf( + s__(`ContainerRegistry|With the Docker Container Registry integrated into GitLab, every + project can have its own space to store its Docker images. Learn more about the + %{docLinkStart}Container Registry%{docLinkEnd}.`), + { + docLinkStart: `<a href="${this.helpPagePath}">`, + docLinkEnd: '</a>', + }, + false, + ); + }, + noContainerImagesText() { + return sprintf( + s__(`ContainerRegistry|With the Container Registry, every project can have its own space to + store its Docker images. Learn more about the %{docLinkStart}Container Registry%{docLinkEnd}.`), + { + docLinkStart: `<a href="${this.helpPagePath}">`, + docLinkEnd: '</a>', + }, + false, + ); + }, }, created() { this.setMainEndpoint(this.endpoint); @@ -33,20 +92,44 @@ export default { </script> <template> <div> - <gl-loading-icon v-if="isLoading" size="md" /> + <svg-message v-if="characterError" id="invalid-characters" :svg-path="containersErrorImage"> + <h4> + {{ s__('ContainerRegistry|Docker connection error') }} + </h4> + <p v-html="dockerConnectionErrorText"></p> + </svg-message> + + <gl-loading-icon v-else-if="isLoading" size="md" class="prepend-top-16" /> + + <div v-else-if="!isLoading && !characterError && repos.length"> + <h4>{{ s__('ContainerRegistry|Container Registry') }}</h4> + <p v-html="introText"></p> + <collapsible-container v-for="item in repos" :key="item.id" :repo="item" /> + </div> + + <svg-message + v-else-if="!isLoading && !characterError && !repos.length" + id="no-container-images" + :svg-path="noContainersImage" + > + <h4> + {{ s__('ContainerRegistry|There are no container images stored for this project') }} + </h4> + <p v-html="noContainerImagesText"></p> - <collapsible-container - v-for="item in repos" - v-else-if="!isLoading && repos.length" - :key="item.id" - :repo="item" - /> + <h5>{{ s__('ContainerRegistry|Quick Start') }}</h5> + <p> + {{ + s__( + 'ContainerRegistry|You can add an image to this registry with the following commands:', + ) + }} + </p> - <p v-else-if="!isLoading && !repos.length"> - {{ - __(`No container images stored for this project. - Add one by following the instructions above.`) - }} - </p> + <pre> + docker build -t {{ repositoryUrl }} . + docker push {{ repositoryUrl }} + </pre> + </svg-message> </div> </template> diff --git a/app/assets/javascripts/registry/components/svg_message.vue b/app/assets/javascripts/registry/components/svg_message.vue new file mode 100644 index 00000000000..d0d44bf2d14 --- /dev/null +++ b/app/assets/javascripts/registry/components/svg_message.vue @@ -0,0 +1,24 @@ +<script> +export default { + name: 'RegistrySvgMessage', + props: { + id: { + type: String, + required: true, + }, + svgPath: { + type: String, + required: true, + }, + }, +}; +</script> + +<template> + <div :id="id" class="empty-state container-message mw-70p"> + <div class="svg-content"> + <img :src="svgPath" class="flex-align-self-center" /> + </div> + <slot></slot> + </div> +</template> diff --git a/app/assets/javascripts/registry/index.js b/app/assets/javascripts/registry/index.js index 025afefe7f0..d8daec29fda 100644 --- a/app/assets/javascripts/registry/index.js +++ b/app/assets/javascripts/registry/index.js @@ -14,12 +14,22 @@ export default () => const { dataset } = document.querySelector(this.$options.el); return { endpoint: dataset.endpoint, + characterError: Boolean(dataset.characterError), + helpPagePath: dataset.helpPagePath, + noContainersImage: dataset.noContainersImage, + containersErrorImage: dataset.containersErrorImage, + repositoryUrl: dataset.repositoryUrl, }; }, render(createElement) { return createElement('registry-app', { props: { endpoint: this.endpoint, + characterError: this.characterError, + helpPagePath: this.helpPagePath, + noContainersImage: this.noContainersImage, + containersErrorImage: this.containersErrorImage, + repositoryUrl: this.repositoryUrl, }, }); }, diff --git a/app/assets/javascripts/releases/components/release_block.vue b/app/assets/javascripts/releases/components/release_block.vue index f510b905a2e..0031ba04d78 100644 --- a/app/assets/javascripts/releases/components/release_block.vue +++ b/app/assets/javascripts/releases/components/release_block.vue @@ -4,7 +4,7 @@ import { GlTooltipDirective, GlLink, GlBadge } from '@gitlab/ui'; import Icon from '~/vue_shared/components/icon.vue'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import timeagoMixin from '~/vue_shared/mixins/timeago'; -import { sprintf } from '../../locale'; +import { __, sprintf } from '../../locale'; export default { name: 'ReleaseBlock', @@ -27,13 +27,13 @@ export default { }, computed: { releasedTimeAgo() { - return sprintf('released %{time}', { - time: this.timeFormated(this.release.created_at), + return sprintf(__('released %{time}'), { + time: this.timeFormated(this.release.released_at), }); }, userImageAltDescription() { return this.author && this.author.username - ? sprintf("%{username}'s avatar", { username: this.author.username }) + ? sprintf(__("%{username}'s avatar"), { username: this.author.username }) : null; }, commit() { @@ -56,8 +56,8 @@ export default { <div class="card-body"> <h2 class="card-title mt-0"> {{ release.name }} - <gl-badge v-if="release.pre_release" variant="warning" class="align-middle">{{ - __('Pre-release') + <gl-badge v-if="release.upcoming_release" variant="warning" class="align-middle">{{ + __('Upcoming Release') }}</gl-badge> </h2> @@ -74,7 +74,7 @@ export default { <div class="append-right-4"> • - <span v-gl-tooltip.bottom :title="tooltipTitle(release.created_at)"> + <span v-gl-tooltip.bottom :title="tooltipTitle(release.released_at)"> {{ releasedTimeAgo }} </span> </div> diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue index 1e66ccbfa29..0d9e992e596 100644 --- a/app/assets/javascripts/repository/components/table/index.vue +++ b/app/assets/javascripts/repository/components/table/index.vue @@ -76,7 +76,7 @@ export default { variables: { projectPath: this.projectPath, ref: this.ref, - path: this.path, + path: this.path || '/', nextPageCursor: this.nextPageCursor, pageSize: PAGE_SIZE, }, diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue index c31e7fa71a2..3e060e9ecb6 100644 --- a/app/assets/javascripts/repository/components/table/row.vue +++ b/app/assets/javascripts/repository/components/table/row.vue @@ -110,9 +110,7 @@ export default { <component :is="linkComponent" :to="routerLinkTo" :href="url" class="str-truncated"> {{ fullPath }} </component> - <gl-badge v-if="lfsOid" variant="default" class="label-lfs ml-1"> - LFS - </gl-badge> + <gl-badge v-if="lfsOid" variant="default" class="label-lfs ml-1">LFS</gl-badge> <template v-if="isSubmodule"> @ <gl-link href="#" class="commit-sha">{{ shortSha }}</gl-link> </template> diff --git a/app/assets/javascripts/serverless/components/area.vue b/app/assets/javascripts/serverless/components/area.vue index 32c9d6eccb8..a1a8cd3acbd 100644 --- a/app/assets/javascripts/serverless/components/area.vue +++ b/app/assets/javascripts/serverless/components/area.vue @@ -4,6 +4,7 @@ import { debounceByAnimationFrame } from '~/lib/utils/common_utils'; import dateFormat from 'dateformat'; import { X_INTERVAL } from '../constants'; import { validateGraphData } from '../utils'; +import { __ } from '~/locale'; let debouncedResize; @@ -42,7 +43,7 @@ export default { }, generateSeries() { return { - name: 'Invocations', + name: __('Invocations'), type: 'line', data: this.chartData.requests.map(data => [data.time, data.value]), symbolSize: 0, @@ -124,7 +125,9 @@ export default { <div class="prometheus-graph"> <div class="prometheus-graph-header"> <h5 ref="graphTitle" class="prometheus-graph-title">{{ graphData.title }}</h5> - <div ref="graphWidgets" class="prometheus-graph-widgets"><slot></slot></div> + <div ref="graphWidgets" class="prometheus-graph-widgets"> + <slot></slot> + </div> </div> <gl-area-chart ref="areaChart" @@ -135,12 +138,8 @@ export default { :width="width" :include-legend-avg-max="false" > - <template slot="tooltipTitle"> - {{ tooltipPopoverTitle }} - </template> - <template slot="tooltipContent"> - {{ tooltipPopoverContent }} - </template> + <template slot="tooltipTitle">{{ tooltipPopoverTitle }}</template> + <template slot="tooltipContent">{{ tooltipPopoverContent }}</template> </gl-area-chart> </div> </template> diff --git a/app/assets/javascripts/serverless/components/function_details.vue b/app/assets/javascripts/serverless/components/function_details.vue index b8906cfca4e..d542dad8119 100644 --- a/app/assets/javascripts/serverless/components/function_details.vue +++ b/app/assets/javascripts/serverless/components/function_details.vue @@ -89,7 +89,9 @@ export default { }} </p> </div> - <div v-else><p>No pods loaded at this time.</p></div> + <div v-else> + <p>{{ s__('ServerlessDetails|No pods loaded at this time.') }}</p> + </div> <area-chart v-if="hasPrometheusData" :graph-data="graphData" :container-width="elWidth" /> <missing-prometheus diff --git a/app/assets/javascripts/serverless/components/functions.vue b/app/assets/javascripts/serverless/components/functions.vue index 94341050b86..9e66869515c 100644 --- a/app/assets/javascripts/serverless/components/functions.vue +++ b/app/assets/javascripts/serverless/components/functions.vue @@ -1,4 +1,5 @@ <script> +import { sprintf, s__ } from '~/locale'; import { mapState, mapActions, mapGetters } from 'vuex'; import { GlLoadingIcon } from '@gitlab/ui'; import FunctionRow from './function_row.vue'; @@ -37,6 +38,28 @@ export default { isInstalled() { return this.installed === true; }, + noServerlessConfigFile() { + return sprintf( + s__( + 'Serverless|Your repository does not have a corresponding %{startTag}serverless.yml%{endTag} file.', + ), + { startTag: '<code>', endTag: '</code>' }, + ); + }, + noGitlabYamlConfigured() { + return sprintf( + s__('Serverless|Your %{startTag}.gitlab-ci.yml%{endTag} file is not properly configured.'), + { startTag: '<code>', endTag: '</code>' }, + ); + }, + mismatchedServerlessFunctions() { + return sprintf( + s__( + "Serverless|The functions listed in the %{startTag}serverless.yml%{endTag} file don't match the namespace of your cluster.", + ), + { startTag: '<code>', endTag: '</code>' }, + ); + }, }, created() { this.fetchFunctions({ @@ -82,25 +105,29 @@ export default { <h4 class="state-title text-center">{{ s__('Serverless|No functions available') }}</h4> <p class="state-description"> {{ - s__(`Serverless|There is currently no function data available from Knative. - This could be for a variety of reasons including:`) + s__( + 'Serverless|There is currently no function data available from Knative. This could be for a variety of reasons including:', + ) }} </p> <ul> - <li>Your repository does not have a corresponding <code>serverless.yml</code> file.</li> - <li>Your <code>.gitlab-ci.yml</code> file is not properly configured.</li> <li> - The functions listed in the <code>serverless.yml</code> file don't match the namespace - of your cluster. + {{ noServerlessConfigFile }} + </li> + <li> + {{ noGitlabYamlConfigured }} + </li> + <li> + {{ mismatchedServerlessFunctions }} </li> - <li>The deploy job has not finished.</li> + <li>{{ s__('Serverless|The deploy job has not finished.') }}</li> </ul> <p> {{ - s__(`Serverless|If you believe none of these apply, please check - back later as the function data may be in the process of becoming - available.`) + s__( + 'Serverless|If you believe none of these apply, please check back later as the function data may be in the process of becoming available.', + ) }} </p> <div class="text-center"> diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue index 0ad2b3a73a2..fa6b6bfaef1 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue @@ -1,4 +1,6 @@ <script> +import { n__ } from '~/locale'; + export default { name: 'AssigneeTitle', props: { @@ -24,7 +26,7 @@ export default { computed: { assigneeTitle() { const assignees = this.numberOfAssignees; - return assignees > 1 ? `${assignees} Assignees` : 'Assignee'; + return n__('Assignee', `%d Assignees`, assignees); }, }, }; @@ -32,18 +34,18 @@ export default { <template> <div class="title hide-collapsed"> {{ assigneeTitle }} - <i v-if="loading" aria-hidden="true" class="fa fa-spinner fa-spin block-loading"> </i> + <i v-if="loading" aria-hidden="true" class="fa fa-spinner fa-spin block-loading"></i> <a v-if="editable" class="js-sidebar-dropdown-toggle edit-link float-right" href="#"> {{ __('Edit') }} </a> <a v-if="showToggle" - aria-label="Toggle sidebar" + :aria-label="__('Toggle sidebar')" class="gutter-toggle float-right js-sidebar-toggle" href="#" role="button" > - <i aria-hidden="true" data-hidden="true" class="fa fa-angle-double-right"> </i> + <i aria-hidden="true" data-hidden="true" class="fa fa-angle-double-right"></i> </a> </div> </template> diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue index 0074d7099dc..805c21d0965 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue @@ -1,5 +1,5 @@ <script> -import { __ } from '~/locale'; +import { __, sprintf } from '~/locale'; import tooltip from '~/vue_shared/directives/tooltip'; export default { @@ -62,7 +62,8 @@ export default { return this.numberOfHiddenAssignees > 0; }, hiddenAssigneesLabel() { - return `+ ${this.numberOfHiddenAssignees} more`; + const { numberOfHiddenAssignees } = this; + return sprintf(__('+ %{numberOfHiddenAssignees} more'), { numberOfHiddenAssignees }); }, collapsedTooltipTitle() { const maxRender = Math.min(this.defaultRenderCount, this.users.length); @@ -103,12 +104,15 @@ export default { // Everyone can merge return null; } else if (cannotMergeCount === assigneesCount && assigneesCount > 1) { - return 'No one can merge'; + return __('No one can merge'); } else if (assigneesCount === 1) { - return 'Cannot merge'; + return __('Cannot merge'); } - return `${canMergeCount}/${assigneesCount} can merge`; + return sprintf(__('%{canMergeCount}/%{assigneesCount} can merge'), { + canMergeCount, + assigneesCount, + }); }, }, methods: { @@ -128,7 +132,7 @@ export default { return `${this.rootPath}${user.username}`; }, assigneeAlt(user) { - return `${user.name}'s avatar`; + return sprintf(__("%{userName}'s avatar"), { userName: user.name }); }, assigneeUsername(user) { return `@${user.username}`; @@ -153,7 +157,7 @@ export default { data-placement="left" data-boundary="viewport" > - <i v-if="hasNoUsers" aria-label="None" class="fa fa-user"> </i> + <i v-if="hasNoUsers" :aria-label="__('None')" class="fa fa-user"> </i> <button v-for="(user, index) in users" v-if="shouldRenderCollapsedAssignee(index)" @@ -185,9 +189,12 @@ export default { </span> <template v-if="hasNoUsers"> <span class="assign-yourself no-value qa-assign-yourself"> - None + {{ __('None') }} <template v-if="editable"> - - <button type="button" class="btn-link" @click="assignSelf">assign yourself</button> + - + <button type="button" class="btn-link" @click="assignSelf"> + {{ __('assign yourself') }} + </button> </template> </span> </template> @@ -232,9 +239,7 @@ export default { <template v-if="showLess"> {{ hiddenAssigneesLabel }} </template> - <template v-else> - - show less - </template> + <template v-else>{{ __('- show less') }}</template> </button> </div> </template> diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue index cfa7029b388..be1e4811856 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue @@ -2,8 +2,10 @@ import Flash from '~/flash'; import eventHub from '~/sidebar/event_hub'; import Store from '~/sidebar/stores/sidebar_store'; +import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; import AssigneeTitle from './assignee_title.vue'; import Assignees from './assignees.vue'; +import { __ } from '~/locale'; export default { name: 'SidebarAssignees', @@ -72,9 +74,12 @@ export default { this.mediator .saveAssignees(this.field) .then(setLoadingFalse.bind(this)) + .then(() => { + refreshUserMergeRequestCounts(); + }) .catch(() => { setLoadingFalse(); - return new Flash('Error occurred when saving assignees'); + return new Flash(__('Error occurred when saving assignees')); }); }, }, diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue index 4b9bb5c7b0e..5d0e39e8195 100644 --- a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue +++ b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue @@ -1,6 +1,7 @@ <script> import $ from 'jquery'; import eventHub from '../../event_hub'; +import { __ } from '~/locale'; export default { props: { @@ -15,7 +16,7 @@ export default { }, computed: { toggleButtonText() { - return this.isConfidential ? 'Turn Off' : 'Turn On'; + return this.isConfidential ? __('Turn Off') : __('Turn On'); }, updateConfidentialBool() { return !this.isConfidential; diff --git a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue index 657ac837baf..24d5b14ded9 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue @@ -79,7 +79,7 @@ export default { } else if (this.showSpentOnlyState) { return `${this.timeSpent} / --`; } else if (this.showNoTimeTrackingState) { - return 'None'; + return __('None'); } return ''; diff --git a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue index bc263bc36e4..06aca547183 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue @@ -2,6 +2,7 @@ import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility'; import tooltip from '../../../vue_shared/directives/tooltip'; import { GlProgressBar } from '@gitlab/ui'; +import { s__, sprintf } from '~/locale'; export default { name: 'TimeTrackingComparisonPane', @@ -43,8 +44,14 @@ export default { return stringifyTime(this.parsedTimeRemaining); }, timeRemainingTooltip() { - const prefix = this.timeRemainingMinutes < 0 ? 'Over by' : 'Time remaining:'; - return `${prefix} ${this.timeRemainingHumanReadable}`; + const { timeRemainingHumanReadable, timeRemainingMinutes } = this; + return timeRemainingMinutes < 0 + ? sprintf(s__('TimeTracking|Over by %{timeRemainingHumanReadable}'), { + timeRemainingHumanReadable, + }) + : sprintf(s__('TimeTracking|Time remaining: %{timeRemainingHumanReadable}'), { + timeRemainingHumanReadable, + }); }, /* Diff values for comparison meter */ timeRemainingMinutes() { @@ -74,12 +81,12 @@ export default { <gl-progress-bar :value="timeRemainingPercent" :variant="progressBarVariant" /> <div class="compare-display-container"> <div class="compare-display float-left"> - <span class="compare-label"> {{ s__('TimeTracking|Spent') }} </span> - <span class="compare-value spent"> {{ timeSpentHumanReadable }} </span> + <span class="compare-label">{{ s__('TimeTracking|Spent') }}</span> + <span class="compare-value spent">{{ timeSpentHumanReadable }}</span> </div> <div class="compare-display estimated float-right"> - <span class="compare-label"> {{ s__('TimeTrackingEstimated|Est') }} </span> - <span class="compare-value"> {{ timeEstimateHumanReadable }} </span> + <span class="compare-label">{{ s__('TimeTrackingEstimated|Est') }}</span> + <span class="compare-value">{{ timeEstimateHumanReadable }}</span> </div> </div> </div> diff --git a/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue index 7c7356e2afa..c2f30310e2e 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue @@ -1,4 +1,6 @@ <script> +import { sprintf, s__ } from '~/locale'; + export default { name: 'TimeTrackingSpentOnlyPane', props: { @@ -7,11 +9,22 @@ export default { required: true, }, }, + computed: { + timeSpent() { + return sprintf( + s__('TimeTracking|%{startTag}Spent: %{endTag}%{timeSpentHumanReadable}'), + { + startTag: '<span class="bold">', + endTag: '</span>', + timeSpentHumanReadable: this.timeSpentHumanReadable, + }, + false, + ); + }, + }, }; </script> <template> - <div class="time-tracking-spend-only-pane"> - <span class="bold">Spent:</span> {{ timeSpentHumanReadable }} - </div> + <div class="time-tracking-spend-only-pane" v-html="timeSpent"></div> </template> diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue index 57125c78cf6..e6f2fe2b5fc 100644 --- a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue +++ b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue @@ -5,8 +5,8 @@ import { GlLoadingIcon } from '@gitlab/ui'; import Icon from '~/vue_shared/components/icon.vue'; -const MARK_TEXT = __('Mark todo as done'); -const TODO_TEXT = __('Add todo'); +const MARK_TEXT = __('Mark as done'); +const TODO_TEXT = __('Add a To Do'); export default { directives: { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue index 34cdb70ce14..5c7859828d8 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue @@ -125,7 +125,9 @@ export default { this.isStopping = false; }) .catch(() => { - createFlash('Something went wrong while stopping this environment. Please try again.'); + createFlash( + __('Something went wrong while stopping this environment. Please try again.'), + ); this.isStopping = false; }); } diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue index e20a16900d4..fb826be19f5 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue @@ -139,7 +139,7 @@ export default { type="button" class="btn dropdown-toggle qa-dropdown-toggle" data-toggle="dropdown" - aria-label="Download as" + :aria-label="__('Download as')" aria-haspopup="true" aria-expanded="false" > diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue index 5958c2cf87e..8e8e67228ed 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue @@ -6,6 +6,7 @@ import statusIcon from '../mr_widget_status_icon.vue'; import MrWidgetAuthor from '../../components/mr_widget_author.vue'; import eventHub from '../../event_hub'; import { AUTO_MERGE_STRATEGIES } from '../../constants'; +import { __ } from '~/locale'; export default { name: 'MRWidgetAutoMergeEnabled', @@ -55,7 +56,7 @@ export default { }) .catch(() => { this.isCancellingAutoMerge = false; - Flash('Something went wrong. Please try again.'); + Flash(__('Something went wrong. Please try again.')); }); }, removeSourceBranch() { @@ -76,7 +77,7 @@ export default { }) .catch(() => { this.isRemovingSourceBranch = false; - Flash('Something went wrong. Please try again.'); + Flash(__('Something went wrong. Please try again.')); }); }, }, @@ -107,15 +108,15 @@ export default { <section class="mr-info-list"> <p> {{ s__('mrWidget|The changes will be merged into') }} - <a :href="mr.targetBranchPath" class="label-branch"> {{ mr.targetBranch }} </a> + <a :href="mr.targetBranchPath" class="label-branch">{{ mr.targetBranch }}</a> </p> <p v-if="mr.shouldRemoveSourceBranch"> {{ s__('mrWidget|The source branch will be deleted') }} </p> <p v-else class="d-flex align-items-start"> - <span class="append-right-10"> - {{ s__('mrWidget|The source branch will not be deleted') }} - </span> + <span class="append-right-10">{{ + s__('mrWidget|The source branch will not be deleted') + }}</span> <a v-if="canRemoveSourceBranch" :disabled="isRemovingSourceBranch" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue index 0bcccc50eb2..c7b064b8506 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue @@ -4,6 +4,7 @@ import simplePoll from '../../../lib/utils/simple_poll'; import eventHub from '../../event_hub'; import statusIcon from '../mr_widget_status_icon.vue'; import Flash from '../../../flash'; +import { __, sprintf } from '~/locale'; export default { name: 'MRWidgetRebase', @@ -40,6 +41,17 @@ export default { showDisabledButton() { return ['failed', 'loading'].includes(this.status); }, + fastForwardMergeText() { + return sprintf( + __( + `Fast-forward merge is not possible. Rebase the source branch onto %{startTag}${this.mr.targetBranch}%{endTag} to allow this merge request to be merged.`, + ), + { + startTag: '<span class="label-branch">', + endTag: '</span>', + }, + ); + }, }, methods: { rebase() { @@ -54,7 +66,7 @@ export default { .catch(error => { this.rebasingError = error.merge_error; this.isMakingRequest = false; - Flash('Something went wrong. Please try again.'); + Flash(__('Something went wrong. Please try again.')); }); }, checkRebaseStatus(continuePolling, stopPolling) { @@ -69,7 +81,7 @@ export default { if (res.merge_error && res.merge_error.length) { this.rebasingError = res.merge_error; - Flash('Something went wrong. Please try again.'); + Flash(__('Something went wrong. Please try again.')); } eventHub.$emit('MRWidgetRebaseSuccess'); @@ -78,7 +90,7 @@ export default { }) .catch(() => { this.isMakingRequest = false; - Flash('Something went wrong. Please try again.'); + Flash(__('Something went wrong. Please try again.')); stopPolling(); }); }, @@ -91,19 +103,14 @@ export default { <div class="rebase-state-find-class-convention media media-body space-children"> <template v-if="mr.rebaseInProgress || isMakingRequest"> - <span class="bold"> Rebase in progress </span> + <span class="bold">{{ __('Rebase in progress') }}</span> </template> <template v-if="!mr.rebaseInProgress && !mr.canPushToSourceBranch"> - <span class="bold"> - Fast-forward merge is not possible. Rebase the source branch onto - <span class="label-branch">{{ mr.targetBranch }}</span> to allow this merge request to be - merged. - </span> + <span class="bold" v-html="fastForwardMergeText"></span> </template> <template v-if="!mr.rebaseInProgress && mr.canPushToSourceBranch && !isMakingRequest"> <div - class="accept-merge-holder clearfix -js-toggle-container accept-action media space-children" + class="accept-merge-holder clearfix js-toggle-container accept-action media space-children" > <button :disabled="isMakingRequest" @@ -111,14 +118,14 @@ js-toggle-container accept-action media space-children" class="btn btn-sm btn-reopen btn-success qa-mr-rebase-button" @click="rebase" > - <gl-loading-icon v-if="isMakingRequest" /> - Rebase + <gl-loading-icon v-if="isMakingRequest" />{{ __('Rebase') }} </button> - <span v-if="!rebasingError" class="bold"> - Fast-forward merge is not possible. Rebase the source branch onto the target branch or - merge target branch into source branch to allow this merge request to be merged. - </span> - <span v-else class="bold danger"> {{ rebasingError }} </span> + <span v-if="!rebasingError" class="bold">{{ + __( + 'Fast-forward merge is not possible. Rebase the source branch onto the target branch or merge target branch into source branch to allow this merge request to be merged.', + ) + }}</span> + <span v-else class="bold danger">{{ rebasingError }}</span> </div> </template> </div> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue index a38495bb4cc..7312b31c01c 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue @@ -22,19 +22,29 @@ export default { <span v-html="emptyStateSVG"></span> </div> <div class="text col-md-7 order-md-first col-12"> - <span> - Merge requests are a place to propose changes you have made to a project and discuss those - changes with others. - </span> - <p>Interested parties can even contribute by pushing commits if they want to.</p> + <span>{{ + s__( + 'mrWidgetNothingToMerge|Merge requests are a place to propose changes you have made to a project and discuss those changes with others.', + ) + }}</span> <p> - Currently there are no changes in this merge request's source branch. Please push new - commits or use a different branch. + {{ + s__( + 'mrWidgetNothingToMerge|Interested parties can even contribute by pushing commits if they want to.', + ) + }} + </p> + <p> + {{ + s__( + "mrWidgetNothingToMerge|Currently there are no changes in this merge request's source branch. Please push new commits or use a different branch.", + ) + }} </p> <div> - <a v-if="mr.newBlobPath" :href="mr.newBlobPath" class="btn btn-inverted btn-success"> - Create file - </a> + <a v-if="mr.newBlobPath" :href="mr.newBlobPath" class="btn btn-inverted btn-success">{{ + __('Create file') + }}</a> </div> </div> </div> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue index ca1b4a57717..d4514767912 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue @@ -6,6 +6,7 @@ import simplePoll from '~/lib/utils/simple_poll'; import { __ } from '~/locale'; import readyToMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/ready_to_merge'; import MergeRequest from '../../../merge_request'; +import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; import Flash from '../../../flash'; import statusIcon from '../mr_widget_status_icon.vue'; import eventHub from '../../event_hub'; @@ -174,6 +175,8 @@ export default { MergeRequest.decreaseCounter(); stopPolling(); + refreshUserMergeRequestCounts(); + // If user checked remove source branch and we didn't remove the branch yet // we should start another polling for source branch remove process if (this.removeSourceBranch && data.source_branch_exists) { @@ -248,7 +251,7 @@ export default { type="button" class="btn btn-sm btn-info dropdown-toggle js-merge-moment" data-toggle="dropdown" - aria-label="Select merge moment" + :aria-label="__('Select merge moment')" > <i class="fa fa-chevron-down qa-merge-moment-dropdown" aria-hidden="true"></i> </button> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue index a9fb40a4949..d4a5fdb4b97 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue @@ -20,7 +20,7 @@ export default { <status-icon :show-disabled-button="true" status="warning" /> <div class="media-body space-children"> <span class="bold"> - {{ s__('mrWidget|There are unresolved discussions. Please resolve these discussions') }} + {{ s__('mrWidget|There are unresolved threads. Please resolve these threads') }} </span> <a v-if="mr.createIssueToResolveDiscussionsPath" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue index 7c322388d30..91c0b40a0b5 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue @@ -46,14 +46,20 @@ export default { <status-icon :show-disabled-button="Boolean(mr.removeWIPPath)" status="warning" /> <div class="media-body space-children"> <span class="bold"> - This is a Work in Progress + {{ __('This is a Work in Progress') }} <i v-tooltip class="fa fa-question-circle" - title="When this merge request is ready, - remove the WIP: prefix from the title to allow it to be merged" - aria-label="When this merge request is ready, - remove the WIP: prefix from the title to allow it to be merged" + :title=" + s__( + 'mrWidget|When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged', + ) + " + :aria-label=" + s__( + 'mrWidget|When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged', + ) + " > </i> </span> @@ -64,8 +70,8 @@ export default { class="btn btn-default btn-sm js-remove-wip" @click="removeWIP" > - <i v-if="isMakingRequest" class="fa fa-spinner fa-spin" aria-hidden="true"> </i> Resolve WIP - status + <i v-if="isMakingRequest" class="fa fa-spinner fa-spin" aria-hidden="true"> </i> + {{ s__('mrWidget|Resolve WIP status') }} </button> </div> </div> diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index a79da476890..8d415c1bbea 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -263,8 +263,11 @@ export default { if (!data.pipeline) return; const { label } = data.pipeline.details.status; - const title = `Pipeline ${label}`; - const message = `Pipeline ${label} for "${data.title}"`; + const title = sprintf(__('Pipeline %{label}'), { label }); + const message = sprintf(__('Pipeline %{label} for "%{dataTitle}"'), { + dataTitle: data.title, + label, + }); notify.notifyMe(title, message, this.mr.gitlabLogo); }, diff --git a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue index e9ab6f5ba7a..15cb0bd9792 100644 --- a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue +++ b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue @@ -1,7 +1,6 @@ <script> import { GlTooltipDirective } from '@gitlab/ui'; import Icon from '~/vue_shared/components/icon.vue'; -import { pluralize } from '~/lib/utils/text_utility'; import { __, sprintf } from '~/locale'; import { getCommitIconMap } from '~/ide/utils'; @@ -69,7 +68,7 @@ export default { }); } else if (this.file.changed && this.file.staged) { return sprintf(__('Unstaged and staged %{type}'), { - type: pluralize(type), + type, }); } diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue index a1168fa0f1e..ae9b013d980 100644 --- a/app/assets/javascripts/vue_shared/components/commit.vue +++ b/app/assets/javascripts/vue_shared/components/commit.vue @@ -1,6 +1,7 @@ <script> import _ from 'underscore'; import { GlTooltipDirective, GlLink } from '@gitlab/ui'; +import { __, sprintf } from '~/locale'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; import UserAvatarLink from './user_avatar/user_avatar_link.vue'; import Icon from '../../vue_shared/components/icon.vue'; @@ -129,7 +130,9 @@ export default { * @returns {String} */ userImageAltDescription() { - return this.author && this.author.username ? `${this.author.username}'s avatar` : null; + return this.author && this.author.username + ? sprintf(__("%{username}'s avatar"), { username: this.author.username }) + : null; }, }, }; @@ -180,7 +183,7 @@ export default { {{ title }} </gl-link> </tooltip-on-truncate> - <span v-else> Can't find HEAD commit for this branch </span> + <span v-else>{{ __("Can't find HEAD commit for this branch") }}</span> </div> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js b/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js index ba63683f5c0..da0b45110e2 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js +++ b/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js @@ -3,6 +3,7 @@ import { __ } from '~/locale'; const viewers = { image: { id: 'image', + binary: true, }, markdown: { id: 'markdown', diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue index 2ca933a37d2..fc6a45b957e 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue @@ -91,7 +91,9 @@ export default { | </template> <template v-if="hasDimensions"> - <strong>W</strong>: {{ width }} | <strong>H</strong>: {{ height }} + <strong>{{ s__('ImageViewerDimensions|W') }}</strong + >: {{ width }} | <strong>{{ s__('ImageViewerDimensions|H') }}</strong + >: {{ height }} </template> </p> </div> diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue index 5fdc915fffb..655f0054887 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue @@ -40,7 +40,7 @@ export default { this.fetchMarkdownPreview(); }, destroyed() { - if (this.isLoading) axiosSource.cancel('Cancelling Preview'); + if (this.isLoading) axiosSource.cancel(__('Cancelling Preview')); }, methods: { fetchMarkdownPreview() { diff --git a/app/assets/javascripts/vue_shared/components/deprecated_modal.vue b/app/assets/javascripts/vue_shared/components/deprecated_modal.vue index 36b3ee05456..d5558d93219 100644 --- a/app/assets/javascripts/vue_shared/components/deprecated_modal.vue +++ b/app/assets/javascripts/vue_shared/components/deprecated_modal.vue @@ -1,5 +1,7 @@ <script> /* eslint-disable vue/require-default-prop */ +import { __ } from '~/locale'; + export default { name: 'DeprecatedModal', // use GlModal instead @@ -39,7 +41,7 @@ export default { closeButtonLabel: { type: String, required: false, - default: 'Cancel', + default: __('Cancel'), }, primaryButtonLabel: { type: String, @@ -94,7 +96,7 @@ export default { type="button" class="close float-right" data-dismiss="modal" - aria-label="Close" + :aria-label="__('Close')" @click="emitCancel($event)" > <span aria-hidden="true">×</span> diff --git a/app/assets/javascripts/vue_shared/components/droplab_dropdown_button.vue b/app/assets/javascripts/vue_shared/components/droplab_dropdown_button.vue index 7d49c87271d..c35fee84771 100644 --- a/app/assets/javascripts/vue_shared/components/droplab_dropdown_button.vue +++ b/app/assets/javascripts/vue_shared/components/droplab_dropdown_button.vue @@ -69,7 +69,7 @@ export default { data-display="static" data-toggle="dropdown" > - <icon name="arrow-down" aria-label="toggle dropdown" /> + <icon name="arrow-down" :aria-label="__('toggle dropdown')" /> </button> <ul :class="dropdownClass" class="dropdown-menu dropdown-open-top"> <template v-for="(action, index) in actions"> diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue index 1bfa91500cb..fe5289ff371 100644 --- a/app/assets/javascripts/vue_shared/components/file_row.vue +++ b/app/assets/javascripts/vue_shared/components/file_row.vue @@ -131,7 +131,7 @@ export default { </script> <template> - <div> + <div v-if="!file.moved"> <file-header v-if="file.isHeader" :path="file.path" /> <div v-else diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue b/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue index 4e5dfbf3bf8..20bcceeb477 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue @@ -115,7 +115,7 @@ export default { data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" - aria-label="Expand dropdown" + :aria-label="__('Expand dropdown')" > <icon name="angle-down" :size="12" /> </button> @@ -125,7 +125,7 @@ export default { ref="searchInput" v-model="filter" type="search" - placeholder="Filter" + :placeholder="__('Filter')" class="js-filtered-dropdown-input dropdown-input-field" /> <icon class="dropdown-input-search" name="search" /> diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue index 3f45dc7853b..c652a684d7c 100644 --- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue +++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue @@ -1,5 +1,6 @@ <script> import { GlTooltipDirective, GlLink, GlButton } from '@gitlab/ui'; +import { __, sprintf } from '~/locale'; import CiIconBadge from './ci_badge_link.vue'; import TimeagoTooltip from './time_ago_tooltip.vue'; import UserAvatarImage from './user_avatar/user_avatar_image.vue'; @@ -65,7 +66,7 @@ export default { computed: { userAvatarAltText() { - return `${this.user.name}'s avatar`; + return sprintf(__(`%{username}'s avatar`), { username: this.user.name }); }, }, @@ -87,16 +88,12 @@ export default { <strong> {{ itemName }} #{{ itemId }} </strong> - <template v-if="shouldRenderTriggeredLabel"> - triggered - </template> - <template v-else> - created - </template> + <template v-if="shouldRenderTriggeredLabel">{{ __('triggered') }}</template> + <template v-else>{{ __('created') }}</template> <timeago-tooltip :time="time" /> - by + {{ __('by') }} <template v-if="user"> <gl-link diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue index e438ff16a41..47f0851f650 100644 --- a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue +++ b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue @@ -1,7 +1,7 @@ <script> import { GlLink } from '@gitlab/ui'; import _ from 'underscore'; -import { sprintf } from '~/locale'; +import { __, sprintf } from '~/locale'; import icon from '../../../vue_shared/components/icon.vue'; function buildDocsLinkStart(path) { @@ -47,7 +47,9 @@ export default { }, confidentialAndLockedDiscussionText() { return sprintf( - 'This issue is %{confidentialLinkStart}confidential%{linkEnd} and %{lockedLinkStart}locked%{linkEnd}.', + __( + 'This issue is %{confidentialLinkStart}confidential%{linkEnd} and %{lockedLinkStart}locked%{linkEnd}.', + ), { confidentialLinkStart: buildDocsLinkStart(this.confidentialIssueDocsPath), lockedLinkStart: buildDocsLinkStart(this.lockedIssueDocsPath), @@ -66,7 +68,7 @@ export default { <span v-if="isLockedAndConfidential"> <span v-html="confidentialAndLockedDiscussionText"></span> {{ - __(`People without permission will never get a notification and won't be able to comment.`) + __("People without permission will never get a notification and won't be able to comment.") }} </span> diff --git a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue index eb0f666422f..b76679960ca 100644 --- a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue +++ b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue @@ -160,8 +160,8 @@ export default { :disabled="removeDisabled" type="button" class="btn btn-default btn-svg btn-item-remove js-issue-item-remove-button qa-remove-issue-button mr-xl-0 align-self-xl-center" - title="Remove" - aria-label="Remove" + :title="__('Remove')" + :aria-label="__('Remove')" @click="onRemoveRequest" > <icon :size="16" class="btn-item-remove-icon" name="close" /> diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 3bdc0bb8ebd..b520d302407 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -1,7 +1,7 @@ <script> import $ from 'jquery'; import _ from 'underscore'; -import { __ } from '~/locale'; +import { __, sprintf } from '~/locale'; import { stripHtml } from '~/lib/utils/text_utility'; import Flash from '../../../flash'; import GLForm from '../../../gl_form'; @@ -118,6 +118,18 @@ export default { lineType() { return this.line ? this.line.type : ''; }, + addMultipleToDiscussionWarning() { + return sprintf( + __( + '%{icon}You are about to add %{usersTag} people to the discussion. Proceed with caution.', + ), + { + icon: '<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>', + usersTag: `<strong><span class="js-referenced-users-count">${this.referencedUsers.length}</span></strong>`, + }, + false, + ); + }, }, mounted() { /* @@ -172,7 +184,7 @@ export default { renderMarkdown(data = {}) { this.markdownPreviewLoading = false; - this.markdownPreview = data.body || 'Nothing to preview.'; + this.markdownPreview = data.body || __('Nothing to preview.'); if (data.references) { this.referencedCommands = data.references.commands; @@ -207,7 +219,11 @@ export default { <div v-show="!previewMarkdown" class="md-write-holder"> <div class="zen-backdrop"> <slot name="textarea"></slot> - <a class="zen-control zen-control-leave js-zen-leave" href="#" aria-label="Enter zen mode"> + <a + class="zen-control zen-control-leave js-zen-leave" + href="#" + :aria-label="__('Enter zen mode')" + > <icon :size="32" name="screen-normal" /> </a> <markdown-toolbar @@ -246,13 +262,7 @@ export default { <template v-if="previewMarkdown && !markdownPreviewLoading"> <div v-if="referencedCommands" class="referenced-commands" v-html="referencedCommands"></div> <div v-if="shouldShowReferencedUsers" class="referenced-users"> - <span> - <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> You are about to add - <strong> - <span class="js-referenced-users-count">{{ referencedUsers.length }}</span> - </strong> - people to the discussion. Proceed with caution. - </span> + <span v-html="addMultipleToDiscussionWarning"></span> </div> </template> </div> diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue index 8d3705e1e4a..7f0fcfac071 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue @@ -1,5 +1,6 @@ <script> import Vue from 'vue'; +import { __ } from '~/locale'; import SuggestionDiff from './suggestion_diff.vue'; import Flash from '~/flash'; @@ -56,7 +57,7 @@ export default { const suggestionElements = container.querySelectorAll('.js-render-suggestion'); if (this.lineType === 'old') { - Flash('Unable to apply suggestions to a deleted line.', 'alert', this.$el); + Flash(__('Unable to apply suggestions to a deleted line.'), 'alert', this.$el); } suggestionElements.forEach((suggestionEl, i) => { diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue index d6c398c8946..8ce5b615795 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue @@ -33,13 +33,18 @@ export default { <div class="comment-toolbar clearfix"> <div class="toolbar-text"> <template v-if="!hasQuickActionsDocsPath && markdownDocsPath"> - <gl-link :href="markdownDocsPath" target="_blank" tabindex="-1" - >Markdown is supported</gl-link - > + <gl-link :href="markdownDocsPath" target="_blank" tabindex="-1">{{ + __('Markdown is supported') + }}</gl-link> </template> <template v-if="hasQuickActionsDocsPath && markdownDocsPath"> - <gl-link :href="markdownDocsPath" target="_blank" tabindex="-1">Markdown</gl-link> and - <gl-link :href="quickActionsDocsPath" target="_blank" tabindex="-1">quick actions</gl-link> + <gl-link :href="markdownDocsPath" target="_blank" tabindex="-1">{{ + __('Markdown') + }}</gl-link> + and + <gl-link :href="quickActionsDocsPath" target="_blank" tabindex="-1">{{ + __('quick actions') + }}</gl-link> are supported </template> </div> @@ -57,15 +62,17 @@ export default { <i class="fa fa-file-image-o toolbar-button-icon" aria-hidden="true"></i> </span> <span class="uploading-error-message"></span> - <button class="retry-uploading-link" type="button">Try again</button> or - <button class="attach-new-file markdown-selector" type="button">attach a new file</button> + <button class="retry-uploading-link" type="button">{{ __('Try again') }}</button> or + <button class="attach-new-file markdown-selector" type="button"> + {{ __('attach a new file') }} + </button> </span> <button class="markdown-selector button-attach-file btn-link" tabindex="-1" type="button"> <i class="fa fa-file-image-o toolbar-button-icon" aria-hidden="true"></i - ><span class="text-attach-file">Attach a file</span> + ><span class="text-attach-file">{{ __('Attach a file') }}</span> </button> <button class="btn btn-default btn-sm hide button-cancel-uploading-files" type="button"> - Cancel + {{ __('Cancel') }} </button> </span> </div> diff --git a/app/assets/javascripts/vue_shared/components/memory_graph.vue b/app/assets/javascripts/vue_shared/components/memory_graph.vue index 16f4ff068f6..26d7d8e8866 100644 --- a/app/assets/javascripts/vue_shared/components/memory_graph.vue +++ b/app/assets/javascripts/vue_shared/components/memory_graph.vue @@ -1,4 +1,5 @@ <script> +import { __, sprintf } from '~/locale'; import { getTimeago } from '../../lib/utils/datetime_utility'; export default { @@ -20,7 +21,7 @@ export default { computed: { getFormattedMedian() { const deployedSince = getTimeago().format(this.deploymentTime * 1000); - return `Deployed ${deployedSince}`; + return sprintf(__('Deployed %{deployedSince}'), { deployedSince }); }, }, mounted() { diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue index baed26a157c..af02b8969ee 100644 --- a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue @@ -39,7 +39,7 @@ export default { </script> <template> - <timeline-entry-item class="note being-posted fade-in-half"> + <timeline-entry-item class="note note-wrapper being-posted fade-in-half"> <div class="timeline-icon"> <user-avatar-link :link-href="getUserData.path" diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue index 3c86b7e4c61..d6dfe9eded8 100644 --- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue @@ -103,7 +103,7 @@ export default { <div v-if="hasMoreCommits" class="flex-list"> <div class="system-note-commit-list-toggler flex-row" @click="expanded = !expanded"> <icon :name="toggleIcon" :size="8" class="append-right-5" /> - <span>Toggle commit list</span> + <span>{{ __('Toggle commit list') }}</span> </div> </div> </div> diff --git a/app/assets/javascripts/vue_shared/components/project_avatar/image.vue b/app/assets/javascripts/vue_shared/components/project_avatar/image.vue index b9311d65360..43bbb756805 100644 --- a/app/assets/javascripts/vue_shared/components/project_avatar/image.vue +++ b/app/assets/javascripts/vue_shared/components/project_avatar/image.vue @@ -14,7 +14,7 @@ /> */ - +import { __ } from '~/locale'; import defaultAvatarUrl from 'images/no_avatar.png'; import { placeholderImage } from '../../../lazy_loader'; @@ -39,7 +39,7 @@ export default { imgAlt: { type: String, required: false, - default: 'project avatar', + default: __('project avatar'), }, size: { type: Number, diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue index b5e43da401e..4dcc121496c 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue @@ -85,7 +85,7 @@ export default { @click="toggleSidebar" > <span class="sidebar-collapsed-value"> - <span v-if="showFromText">From</span> <span>{{ dateText('min') }}</span> + <span v-if="showFromText">{{ __('From') }}</span> <span>{{ dateText('min') }}</span> </span> </collapsed-calendar-icon> <div v-if="hasMinAndMaxDates" class="text-center sidebar-collapsed-divider">-</div> @@ -96,7 +96,7 @@ export default { @click="toggleSidebar" > <span class="sidebar-collapsed-value"> - <span v-if="!minDate">Until</span> <span>{{ dateText('max') }}</span> + <span v-if="!minDate">{{ __('Until') }}</span> <span>{{ dateText('max') }}</span> </span> </collapsed-calendar-icon> </div> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue index 45f01a6fced..6caf8bc92c2 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue @@ -74,7 +74,7 @@ export default { return dateInWords(this.selectedDate, true); }, collapsedText() { - return this.selectedDateWords ? this.selectedDateWords : 'None'; + return this.selectedDateWords ? this.selectedDateWords : __('None'); }, }, methods: { @@ -112,7 +112,7 @@ export default { class="btn-blank btn-link btn-primary-hover-link btn-sidebar-action" @click="toggleDatePicker" > - Edit + {{ __('Edit') }} </button> <toggle-sidebar v-if="showToggleSidebar" :collapsed="collapsed" @toggle="toggleSidebar" /> </div> @@ -137,11 +137,11 @@ export default { class="btn-blank btn-link btn-secondary-hover-link" @click="newDateSelected(null)" > - remove + {{ __('remove') }} </button> </span> </template> - <span v-else class="no-value"> None </span> + <span v-else class="no-value">{{ __('None') }}</span> </span> </div> </div> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue index 3b5ce0e9910..913c971a512 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue @@ -48,7 +48,7 @@ export default { 'fa-angle-double-right': !collapsed, 'fa-angle-double-left': collapsed, }" - aria-label="toggle collapse" + :aria-label="__('toggle collapse')" class="fa" > </i> diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue index a6c1737dcab..ea483416c46 100644 --- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue @@ -17,6 +17,7 @@ import { GlTooltip } from '@gitlab/ui'; import defaultAvatarUrl from 'images/no_avatar.png'; +import { __ } from '~/locale'; import { placeholderImage } from '../../../lazy_loader'; export default { @@ -43,7 +44,7 @@ export default { imgAlt: { type: String, required: false, - default: 'user avatar', + default: __('user avatar'), }, size: { type: Number, diff --git a/app/assets/javascripts/vue_shared/mixins/is_ee.js b/app/assets/javascripts/vue_shared/mixins/is_ee.js deleted file mode 100644 index 8e00d93ef18..00000000000 --- a/app/assets/javascripts/vue_shared/mixins/is_ee.js +++ /dev/null @@ -1,10 +0,0 @@ -import Vue from 'vue'; -import { isEE } from '~/lib/utils/common_utils'; - -Vue.mixin({ - computed: { - isEE() { - return isEE(); - }, - }, -}); diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index cd951f67293..e75c1379dfb 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -288,7 +288,7 @@ padding: 0 1px; a, - button, + button:not(.dropdown-toggle,.ci-action-icon-container), .menu-item { @include dropdown-link; } diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index f75e5b55506..975dca168d5 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -35,7 +35,8 @@ background-color: $modal-body-bg; line-height: $line-height-base; position: relative; - padding: #{3 * $grid-size} #{2 * $grid-size}; + min-height: $modal-body-height; + padding: #{2 * $grid-size} #{6 * $grid-size} #{2 * $grid-size} #{2 * $grid-size}; text-align: left; white-space: normal; @@ -85,9 +86,9 @@ body.modal-open { .modal { background-color: $black-transparent; - @include media-breakpoint-up(md) { + @include media-breakpoint-up(sm) { .modal-dialog { - margin: 30px auto; + margin: 64px auto; } } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index b6a24247d40..406bcda418e 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -805,7 +805,7 @@ $border-color-settings: #e1e1e1; /* Modals */ -$modal-body-height: 134px; +$modal-body-height: 80px; $modal-border-color: #e9ecef; $priority-label-empty-state-width: 114px; diff --git a/app/assets/stylesheets/pages/container_registry.scss b/app/assets/stylesheets/pages/container_registry.scss index dfff3e15556..cca5214a508 100644 --- a/app/assets/stylesheets/pages/container_registry.scss +++ b/app/assets/stylesheets/pages/container_registry.scss @@ -2,6 +2,12 @@ * Container Registry */ +.container-message { + pre { + white-space: pre-line; + } +} + .container-image { border-bottom: 1px solid $white-normal; } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index d2d35d91e0b..3ffe8ae304d 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -1093,6 +1093,21 @@ table.code { line-height: 0; } +.discussion-collapsible { + margin: 0 $gl-padding $gl-padding 71px; + + .notes { + border-radius: $border-radius-default; + } +} + +.parallel { + .discussion-collapsible { + margin: $gl-padding; + margin-top: 0; + } +} + @media (max-width: map-get($grid-breakpoints, md)-1) { .diffs .files { @include fixed-width-container; @@ -1110,6 +1125,11 @@ table.code { padding-right: 0; } } + + .discussion-collapsible { + margin: $gl-padding; + margin-top: 0; + } } .image-diff-overlay, diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index e880b941d67..b9b8eabf909 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -134,6 +134,15 @@ $note-form-margin-left: 72px; } } + .discussion-toggle-replies { + border-top: 0; + border-radius: 4px 4px 0 0; + + &.collapsed { + border-radius: 4px; + } + } + .note-created-ago, .note-updated-at { white-space: normal; @@ -396,7 +405,7 @@ $note-form-margin-left: 72px; border-radius: 0; @media (min-width: map-get($grid-breakpoints, md)) { - top: 91px; + top: $mr-tabs-height + $header-height; .with-performance-bar & { top: 126px; @@ -462,6 +471,14 @@ $note-form-margin-left: 72px; position: relative; } + .notes-content .discussion-notes.diff-discussions { + border-bottom: 1px solid $border-color; + + &:nth-last-child(1) { + border-bottom: 0; + } + } + .notes_holder { font-family: $regular-font; @@ -517,6 +534,17 @@ $note-form-margin-left: 72px; .discussion-reply-holder { border-radius: 0 0 $border-radius-default $border-radius-default; position: relative; + + .discussion-form { + width: 100%; + background-color: $gray-light; + padding: 0; + } + + .disabled-comment { + padding: $gl-vert-padding 0; + width: 100%; + } } } @@ -569,7 +597,8 @@ $note-form-margin-left: 72px; } .discussion-header { - min-height: 74px; + min-height: $line-height-base * 2em; + box-sizing: content-box; .note-header-info { padding-bottom: 0; @@ -579,13 +608,10 @@ $note-form-margin-left: 72px; overflow-x: auto; overflow-y: hidden; } -} -.unresolved { - .discussion-header { - .note-header-info { - margin-top: $gl-padding-8; - } + &.note-wrapper { + display: flex; + align-items: center; } } diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index 10120a472d3..60400f10ca5 100644 --- a/app/assets/stylesheets/pages/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss @@ -168,6 +168,10 @@ } ul.wiki-pages-list.content-list { + a { + color: $blue-600; + } + ul { list-style: none; margin-left: 0; |