diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-26 15:09:15 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-26 15:09:15 +0300 |
commit | c0dd450008c1cee260905e54bbed202891158697 (patch) | |
tree | 0b034d5e4568303b77302325fc8656bb040b90b4 | |
parent | 26bba9525deb5e9d05fd29cf5b286e7a65d1c791 (diff) |
Add latest changes from gitlab-org/gitlab@master
62 files changed, 515 insertions, 129 deletions
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue b/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue index a071e011ec3..417ecd7f3b4 100644 --- a/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue +++ b/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue @@ -21,6 +21,8 @@ import { import getCurrentIntegrationQuery from '../graphql/queries/get_current_integration.query.graphql'; export const i18n = { + deleteIntegration: s__('AlertSettings|Delete integration'), + editIntegration: s__('AlertSettings|Edit integration'), title: s__('AlertsIntegrations|Current integrations'), emptyState: s__('AlertsIntegrations|No integrations have been added yet'), status: { @@ -174,11 +176,16 @@ export default { <template #cell(actions)="{ item }"> <gl-button-group class="gl-ml-3"> - <gl-button icon="settings" @click="editIntegration(item)" /> + <gl-button + icon="settings" + :aria-label="$options.i18n.editIntegration" + @click="editIntegration(item)" + /> <gl-button v-gl-modal.deleteIntegration :disabled="item.type === $options.typeSet.prometheus" icon="remove" + :aria-label="$options.i18n.deleteIntegration" @click="setIntegrationToDelete(item)" /> </gl-button-group> @@ -198,8 +205,8 @@ export default { </gl-table> <gl-modal modal-id="deleteIntegration" - :title="s__('AlertSettings|Delete integration')" - :ok-title="s__('AlertSettings|Delete integration')" + :title="$options.i18n.deleteIntegration" + :ok-title="$options.i18n.deleteIntegration" ok-variant="danger" @ok="deleteIntegration" > diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 8dc44231e26..0015044bb66 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -44,7 +44,7 @@ const Api = { projectMilestonesPath: '/api/:version/projects/:id/milestones', projectIssuePath: '/api/:version/projects/:id/issues/:issue_iid', mergeRequestsPath: '/api/:version/merge_requests', - groupLabelsPath: '/groups/:namespace_path/-/labels', + groupLabelsPath: '/api/:version/groups/:namespace_path/labels', issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key', issuableTemplatesPath: '/:namespace_path/:project_path/templates/:type', projectTemplatePath: '/api/:version/projects/:id/templates/:type/:key', @@ -402,18 +402,29 @@ const Api = { newLabel(namespacePath, projectPath, data, callback) { let url; + let payload; if (projectPath) { url = Api.buildUrl(Api.projectLabelsPath) .replace(':namespace_path', namespacePath) .replace(':project_path', projectPath); + payload = { + label: data, + }; } else { url = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespacePath); + + // groupLabelsPath uses public API which accepts + // `name` and `color` props. + payload = { + name: data.title, + color: data.color, + }; } return axios .post(url, { - label: data, + ...payload, }) .then((res) => callback(res.data)) .catch((e) => callback(e.response.data)); diff --git a/app/assets/javascripts/badges/components/badge.vue b/app/assets/javascripts/badges/components/badge.vue index 9e5d70075f3..309af368df9 100644 --- a/app/assets/javascripts/badges/components/badge.vue +++ b/app/assets/javascripts/badges/components/badge.vue @@ -1,7 +1,11 @@ <script> import { GlLoadingIcon, GlTooltipDirective, GlIcon, GlButton } from '@gitlab/ui'; +import { s__ } from '~/locale'; export default { + i18n: { + buttonLabel: s__('Badges|Reload badge image'), + }, // name: 'Badge' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/25 // eslint-disable-next-line @gitlab/require-i18n-strings name: 'Badge', @@ -94,7 +98,8 @@ export default { <gl-button v-show="hasError" v-gl-tooltip.hover - :title="s__('Badges|Reload badge image')" + :title="$options.i18n.buttonLabel" + :aria-label="$options.i18n.buttonLabel" category="tertiary" variant="confirm" type="button" diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue index 8c354c61fa2..e5eec70c544 100644 --- a/app/assets/javascripts/boards/components/board_form.vue +++ b/app/assets/javascripts/boards/components/board_form.vue @@ -163,6 +163,9 @@ export default { currentMutation() { return this.board.id ? updateBoardMutation : createBoardMutation; }, + deleteMutation() { + return destroyBoardMutation; + }, baseMutationVariables() { const { board } = this; const variables = { @@ -244,17 +247,20 @@ export default { return this.boardUpdateResponse(response.data); }, + async deleteBoard() { + await this.$apollo.mutate({ + mutation: this.deleteMutation, + variables: { + id: fullBoardId(this.board.id), + }, + }); + }, async submit() { if (this.board.name.length === 0) return; this.isLoading = true; if (this.isDeleteForm) { try { - await this.$apollo.mutate({ - mutation: destroyBoardMutation, - variables: { - id: fullBoardId(this.board.id), - }, - }); + await this.deleteBoard(); visitUrl(this.rootPath); } catch { Flash(this.$options.i18n.deleteErrorMessage); diff --git a/app/assets/javascripts/boards/components/toggle_focus.vue b/app/assets/javascripts/boards/components/toggle_focus.vue index dc0f8ffd0fd..87a1d517111 100644 --- a/app/assets/javascripts/boards/components/toggle_focus.vue +++ b/app/assets/javascripts/boards/components/toggle_focus.vue @@ -47,6 +47,7 @@ export default { class="js-focus-mode-btn" data-qa-selector="focus_mode_button" :title="$options.i18n.toggleFocusMode" + :aria-label="$options.i18n.toggleFocusMode" @click="toggleFocusMode" /> </div> diff --git a/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue b/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue index 0233ffaccdc..bc1e401d373 100644 --- a/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue +++ b/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue @@ -7,6 +7,10 @@ import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; export default { + i18n: { + editButton: s__('Pipelines|Edit'), + revokeButton: s__('Pipelines|Revoke'), + }, components: { GlTable, GlButton, @@ -108,13 +112,15 @@ export default { </template> <template #cell(actions)="{ item }"> <gl-button - :title="s__('Pipelines|Edit')" + :title="$options.i18n.editButton" + :aria-label="$options.i18n.editButton" icon="pencil" data-testid="edit-btn" :href="item.editProjectTriggerPath" /> <gl-button - :title="s__('Pipelines|Revoke')" + :title="$options.i18n.revokeButton" + :aria-label="$options.i18n.revokeButton" icon="remove" variant="warning" :data-confirm=" diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js index b1d486c5d66..8ca4dc587a8 100644 --- a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js +++ b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js @@ -2,6 +2,7 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus'; import $ from 'jquery'; +import { debounce } from 'lodash'; import { isObject } from '~/lib/utils/type_utility'; const BLUR_KEYCODES = [27, 40]; @@ -11,13 +12,21 @@ const HAS_VALUE_CLASS = 'has-value'; export class GitLabDropdownFilter { constructor(input, options) { let ref; - let timeout; this.input = input; this.options = options; // eslint-disable-next-line no-cond-assign this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true; const $inputContainer = this.input.parent(); const $clearButton = $inputContainer.find('.js-dropdown-input-clear'); + const filterRemoteDebounced = debounce(() => { + $inputContainer.parent().addClass('is-loading'); + + return this.options.query(this.input.val(), (data) => { + $inputContainer.parent().removeClass('is-loading'); + return this.options.callback(data); + }); + }, 500); + $clearButton.on('click', (e) => { // Clear click e.preventDefault(); @@ -25,7 +34,6 @@ export class GitLabDropdownFilter { return this.input.val('').trigger('input').focus(); }); // Key events - timeout = ''; this.input .on('keydown', (e) => { const keyCode = e.which; @@ -41,16 +49,7 @@ export class GitLabDropdownFilter { } // Only filter asynchronously only if option remote is set if (this.options.remote) { - clearTimeout(timeout); - // eslint-disable-next-line no-return-assign - return (timeout = setTimeout(() => { - $inputContainer.parent().addClass('is-loading'); - - return this.options.query(this.input.val(), (data) => { - $inputContainer.parent().removeClass('is-loading'); - return this.options.callback(data); - }); - }, 250)); + return filterRemoteDebounced(); } return this.filter(this.input.val()); }); diff --git a/app/assets/javascripts/design_management/components/toolbar/design_navigation.vue b/app/assets/javascripts/design_management/components/toolbar/design_navigation.vue index 8535f818b9c..3ebcde817f9 100644 --- a/app/assets/javascripts/design_management/components/toolbar/design_navigation.vue +++ b/app/assets/javascripts/design_management/components/toolbar/design_navigation.vue @@ -12,6 +12,10 @@ import allDesignsMixin from '../../mixins/all_designs'; import { DESIGN_ROUTE_NAME } from '../../router/constants'; export default { + i18n: { + nextButton: s__('DesignManagement|Go to next design'), + previousButton: s__('DesignManagement|Go to previous design'), + }, components: { GlButton, GlButtonGroup, @@ -81,7 +85,8 @@ export default { <gl-button v-gl-tooltip.bottom :disabled="!previousDesign" - :title="s__('DesignManagement|Go to previous design')" + :title="$options.i18n.previousButton" + :aria-label="$options.i18n.previousButton" icon="angle-left" class="js-previous-design" @click="navigateToDesign(previousDesign)" @@ -89,7 +94,8 @@ export default { <gl-button v-gl-tooltip.bottom :disabled="!nextDesign" - :title="s__('DesignManagement|Go to next design')" + :title="$options.i18n.nextButton" + :aria-label="$options.i18n.nextButton" icon="angle-right" class="js-next-design" @click="navigateToDesign(nextDesign)" diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue index 6b1e2bfb34e..3fb9787ac30 100644 --- a/app/assets/javascripts/diffs/components/compare_versions.vue +++ b/app/assets/javascripts/diffs/components/compare_versions.vue @@ -84,6 +84,7 @@ export default { icon="file-tree" class="gl-mr-3 js-toggle-tree-list" :title="toggleFileBrowserTitle" + :aria-label="toggleFileBrowserTitle" :selected="showTreeList" @click="setShowTreeList({ showTreeList: !showTreeList })" /> diff --git a/app/assets/javascripts/environments/components/environment_rollback.vue b/app/assets/javascripts/environments/components/environment_rollback.vue index 397616c654f..c0b4e96cea2 100644 --- a/app/assets/javascripts/environments/components/environment_rollback.vue +++ b/app/assets/javascripts/environments/components/environment_rollback.vue @@ -71,6 +71,7 @@ export default { class="gl-display-none gl-md-display-block text-secondary" :loading="isLoading" :title="title" + :aria-label="title" :icon="isLastDeployment ? 'repeat' : 'redo'" @click="onClick" /> diff --git a/app/assets/javascripts/issuable/components/issuable_by_email.vue b/app/assets/javascripts/issuable/components/issuable_by_email.vue index 6d063b59922..d0ce8c2c34b 100644 --- a/app/assets/javascripts/issuable/components/issuable_by_email.vue +++ b/app/assets/javascripts/issuable/components/issuable_by_email.vue @@ -14,6 +14,9 @@ import { sprintf, __ } from '~/locale'; import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue'; export default { + i18n: { + sendEmail: __('Send email'), + }, name: 'IssuableByEmail', components: { GlButton, @@ -116,7 +119,8 @@ export default { <gl-button v-gl-tooltip.hover :href="mailToLink" - :title="__('Send email')" + :title="$options.i18n.sendEmail" + :aria-label="$options.i18n.sendEmail" icon="mail" data-testid="mail-to-btn" /> diff --git a/app/assets/javascripts/issuable_show/components/issuable_title.vue b/app/assets/javascripts/issuable_show/components/issuable_title.vue index b7ea4a010a3..b96ce0c43f7 100644 --- a/app/assets/javascripts/issuable_show/components/issuable_title.vue +++ b/app/assets/javascripts/issuable_show/components/issuable_title.vue @@ -6,8 +6,12 @@ import { GlTooltipDirective, GlSafeHtmlDirective as SafeHtml, } from '@gitlab/ui'; +import { __ } from '~/locale'; export default { + i18n: { + editTitleAndDescription: __('Edit title and description'), + }, components: { GlIcon, GlButton, @@ -58,7 +62,8 @@ export default { <gl-button v-if="enableEdit" v-gl-tooltip.bottom - :title="__('Edit title and description')" + :title="$options.i18n.editTitleAndDescription" + :aria-label="$options.i18n.editTitleAndDescription" icon="pencil" class="btn-edit js-issuable-edit qa-edit-button" @click="$emit('edit-issuable', $event)" diff --git a/app/assets/javascripts/issue_show/components/title.vue b/app/assets/javascripts/issue_show/components/title.vue index 806d95ca748..5e92211685a 100644 --- a/app/assets/javascripts/issue_show/components/title.vue +++ b/app/assets/javascripts/issue_show/components/title.vue @@ -1,9 +1,13 @@ <script> import { GlButton, GlTooltipDirective, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; +import { __ } from '~/locale'; import eventHub from '../event_hub'; import animateMixin from '../mixins/animate'; export default { + i18n: { + editTitleAndDescription: __('Edit title and description'), + }, components: { GlButton, }, @@ -78,7 +82,8 @@ export default { v-gl-tooltip.bottom icon="pencil" class="btn-edit js-issuable-edit qa-edit-button" - title="Edit title and description" + :title="$options.i18n.editTitleAndDescription" + :aria-label="$options.i18n.editTitleAndDescription" @click="edit" /> </div> diff --git a/app/assets/javascripts/monitoring/components/dashboard_header.vue b/app/assets/javascripts/monitoring/components/dashboard_header.vue index 3c423bea368..05b5b760f0a 100644 --- a/app/assets/javascripts/monitoring/components/dashboard_header.vue +++ b/app/assets/javascripts/monitoring/components/dashboard_header.vue @@ -14,6 +14,7 @@ import { debounce } from 'lodash'; import { mapActions, mapState, mapGetters } from 'vuex'; import invalidUrl from '~/lib/utils/invalid_url'; import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility'; +import { s__ } from '~/locale'; import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue'; import { timeRanges } from '~/vue_shared/constants'; @@ -24,6 +25,9 @@ import DashboardsDropdown from './dashboards_dropdown.vue'; import RefreshButton from './refresh_button.vue'; export default { + i18n: { + metricsSettings: s__('Metrics|Metrics Settings'), + }, components: { GlIcon, GlButton, @@ -282,7 +286,8 @@ export default { data-testid="metrics-settings-button" icon="settings" :href="operationsSettingsPath" - :title="s__('Metrics|Metrics Settings')" + :title="$options.i18n.metricsSettings" + :aria-label="$options.i18n.metricsSettings" /> </div> </template> diff --git a/app/assets/javascripts/monitoring/components/refresh_button.vue b/app/assets/javascripts/monitoring/components/refresh_button.vue index 3daf5b38933..0b80043a92c 100644 --- a/app/assets/javascripts/monitoring/components/refresh_button.vue +++ b/app/assets/javascripts/monitoring/components/refresh_button.vue @@ -9,7 +9,7 @@ import { } from '@gitlab/ui'; import Visibility from 'visibilityjs'; import { mapActions } from 'vuex'; -import { n__, __ } from '~/locale'; +import { n__, __, s__ } from '~/locale'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; @@ -45,6 +45,9 @@ const makeInterval = (length = 0, unit = 's') => { }; export default { + i18n: { + refreshDashboard: s__('Metrics|Refresh dashboard'), + }, components: { GlButtonGroup, GlButton, @@ -148,7 +151,8 @@ export default { v-gl-tooltip class="gl-flex-grow-1" variant="default" - :title="s__('Metrics|Refresh dashboard')" + :title="$options.i18n.refreshDashboard" + :aria-label="$options.i18n.refreshDashboard" icon="retry" @click="refresh" /> 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 cace382ccd6..5f429cbf462 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 @@ -1,7 +1,11 @@ <script> import { GlTooltipDirective, GlButton } from '@gitlab/ui'; +import { s__ } from '~/locale'; export default { + i18n: { + buttonLabel: s__('MergeRequests|Resolve this thread in a new issue'), + }, name: 'ResolveWithIssueButton', components: { GlButton, @@ -23,7 +27,8 @@ export default { <gl-button v-gl-tooltip :href="url" - :title="s__('MergeRequests|Resolve this thread in a new issue')" + :title="$options.i18n.buttonLabel" + :aria-label="$options.i18n.buttonLabel" class="new-issue-for-discussion discussion-create-issue-btn" icon="issue-new" /> diff --git a/app/assets/javascripts/notes/components/sort_discussion.vue b/app/assets/javascripts/notes/components/sort_discussion.vue index ed1f456c174..92c39fbb9f0 100644 --- a/app/assets/javascripts/notes/components/sort_discussion.vue +++ b/app/assets/javascripts/notes/components/sort_discussion.vue @@ -49,18 +49,17 @@ export default { </script> <template> - <div class="gl-mr-3 gl-display-inline-block gl-vertical-align-bottom full-width-mobile"> + <div + data-testid="sort-discussion-filter" + class="gl-mr-3 gl-display-inline-block gl-vertical-align-bottom full-width-mobile" + > <local-storage-sync :value="sortDirection" :storage-key="storageKey" :persist="persistSortOrder" @input="setDiscussionSortDirection({ direction: $event })" /> - <gl-dropdown - :text="dropdownText" - data-testid="sort-discussion-filter" - class="js-dropdown-text full-width-mobile" - > + <gl-dropdown :text="dropdownText" class="js-dropdown-text full-width-mobile"> <gl-dropdown-item v-for="{ text, key, cls } in $options.SORT_OPTIONS" :key="key" diff --git a/app/assets/javascripts/registry/settings/components/settings_form.vue b/app/assets/javascripts/registry/settings/components/settings_form.vue index eb731c382e1..1360e09a75d 100644 --- a/app/assets/javascripts/registry/settings/components/settings_form.vue +++ b/app/assets/javascripts/registry/settings/components/settings_form.vue @@ -110,12 +110,12 @@ export default { mutationVariables() { return { projectPath: this.projectPath, - enabled: this.value.enabled, - cadence: this.value.cadence, - olderThan: this.value.olderThan, - keepN: this.value.keepN, - nameRegex: this.value.nameRegex, - nameRegexKeep: this.value.nameRegexKeep, + enabled: this.prefilledForm.enabled, + cadence: this.prefilledForm.cadence, + olderThan: this.prefilledForm.olderThan, + keepN: this.prefilledForm.keepN, + nameRegex: this.prefilledForm.nameRegex, + nameRegexKeep: this.prefilledForm.nameRegexKeep, }; }, }, @@ -291,8 +291,8 @@ export default { type="submit" :disabled="isSubmitButtonDisabled" :loading="showLoadingIcon" - variant="success" category="primary" + variant="confirm" class="js-no-auto-disable gl-mr-4" > {{ $options.i18n.SET_CLEANUP_POLICY_BUTTON }} diff --git a/app/assets/javascripts/releases/components/release_block_header.vue b/app/assets/javascripts/releases/components/release_block_header.vue index 356fc0f3bf3..89bc314db89 100644 --- a/app/assets/javascripts/releases/components/release_block_header.vue +++ b/app/assets/javascripts/releases/components/release_block_header.vue @@ -1,9 +1,13 @@ <script> import { GlTooltipDirective, GlLink, GlBadge, GlButton, GlIcon } from '@gitlab/ui'; import { setUrlParams } from '~/lib/utils/url_utility'; +import { __ } from '~/locale'; import { BACK_URL_PARAM } from '~/releases/constants'; export default { + i18n: { + editButton: __('Edit this release'), + }, name: 'ReleaseBlockHeader', components: { GlLink, @@ -69,7 +73,8 @@ export default { variant="default" icon="pencil" class="gl-mr-3 js-edit-button ml-2 pb-2" - :title="__('Edit this release')" + :title="$options.i18n.editButton" + :aria-label="$options.i18n.editButton" :href="editLink" /> </div> diff --git a/app/assets/javascripts/search/sort/components/app.vue b/app/assets/javascripts/search/sort/components/app.vue index e4eba655e39..2bf144705c4 100644 --- a/app/assets/javascripts/search/sort/components/app.vue +++ b/app/assets/javascripts/search/sort/components/app.vue @@ -96,6 +96,7 @@ export default { v-gl-tooltip :disabled="!selectedSortOption.sortable" :title="sortDirectionData.tooltip" + :aria-label="sortDirectionData.tooltip" :icon="sortDirectionData.icon" @click="handleSortDirectionChange" /> diff --git a/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue b/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue index dd1d54d67f2..c6fef86c6ff 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue @@ -1,12 +1,15 @@ <script> import { GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui'; -import { sprintf, s__ } from '~/locale'; +import { __, sprintf, s__ } from '~/locale'; import ReviewerAvatarLink from './reviewer_avatar_link.vue'; const LOADING_STATE = 'loading'; const SUCCESS_STATE = 'success'; export default { + i18n: { + reRequestReview: __('Re-request review'), + }, components: { GlButton, GlIcon, @@ -109,7 +112,8 @@ export default { <gl-button v-else-if="user.can_update_merge_request && user.reviewed" v-gl-tooltip.left - :title="__('Re-request review')" + :title="$options.i18n.reRequestReview" + :aria-label="$options.i18n.reRequestReview" :loading="loadingStates[user.id] === $options.LOADING_STATE" class="float-right gl-text-gray-500!" size="small" diff --git a/app/assets/javascripts/snippets/components/embed_dropdown.vue b/app/assets/javascripts/snippets/components/embed_dropdown.vue index f6c9c569b5f..ad1b08a5a07 100644 --- a/app/assets/javascripts/snippets/components/embed_dropdown.vue +++ b/app/assets/javascripts/snippets/components/embed_dropdown.vue @@ -65,6 +65,7 @@ export default { <gl-button v-gl-tooltip.hover :title="$options.MSG_COPY" + :aria-label="$options.MSG_COPY" :data-clipboard-text="value" icon="copy-to-clipboard" data-qa-selector="copy_button" diff --git a/app/assets/javascripts/static_site_editor/graphql/index.js b/app/assets/javascripts/static_site_editor/graphql/index.js index 23f800517c9..2ae2baddbcc 100644 --- a/app/assets/javascripts/static_site_editor/graphql/index.js +++ b/app/assets/javascripts/static_site_editor/graphql/index.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createDefaultClient from '~/lib/graphql'; +import appDataQuery from './queries/app_data.query.graphql'; import fileResolver from './resolvers/file'; import hasSubmittedChangesResolver from './resolvers/has_submitted_changes'; import submitContentChangesResolver from './resolvers/submit_content_changes'; @@ -28,7 +29,8 @@ const createApolloProvider = (appData) => { // eslint-disable-next-line @gitlab/require-i18n-strings const mounts = appData.mounts.map((mount) => ({ __typename: 'Mount', ...mount })); - defaultClient.cache.writeData({ + defaultClient.cache.writeQuery({ + query: appDataQuery, data: { appData: { __typename: 'AppData', diff --git a/app/assets/javascripts/vue_shared/components/clone_dropdown.vue b/app/assets/javascripts/vue_shared/components/clone_dropdown.vue index cd5f63afc79..f14e1992901 100644 --- a/app/assets/javascripts/vue_shared/components/clone_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/clone_dropdown.vue @@ -56,6 +56,7 @@ export default { <gl-button v-gl-tooltip.hover :title="$options.copyURLTooltip" + :aria-label="$options.copyURLTooltip" :data-clipboard-text="sshLink" data-qa-selector="copy_ssh_url_button" icon="copy-to-clipboard" @@ -75,6 +76,7 @@ export default { <gl-button v-gl-tooltip.hover :title="$options.copyURLTooltip" + :aria-label="$options.copyURLTooltip" :data-clipboard-text="httpLink" data-qa-selector="copy_http_url_button" icon="copy-to-clipboard" diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue index 6952c5def8c..107ced550c1 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue @@ -363,6 +363,7 @@ export default { <gl-button v-gl-tooltip :title="sortDirectionTooltip" + :aria-label="sortDirectionTooltip" :icon="sortDirectionIcon" class="flex-shrink-1" @click="handleSortDirectionClick" diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue index 9c2a644b7a9..76b005772ec 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue @@ -46,7 +46,7 @@ export default { }, activeLabel() { return this.labels.find( - (label) => label.title.toLowerCase() === stripQuotes(this.currentValue), + (label) => this.getLabelName(label).toLowerCase() === stripQuotes(this.currentValue), ); }, containerStyle() { @@ -69,6 +69,21 @@ export default { }, }, methods: { + /** + * There's an inconsistency between private and public API + * for labels where label name is included in a different + * property; + * + * Private API => `label.title` + * Public API => `label.name` + * + * This method allows compatibility as there may be instances + * where `config.fetchLabels` provided externally may still be + * using either of the two APIs. + */ + getLabelName(label) { + return label.name || label.title; + }, fetchLabelBySearchTerm(searchTerm) { this.loading = true; this.config @@ -85,7 +100,7 @@ export default { }); }, searchLabels: debounce(function debouncedSearch({ data }) { - this.fetchLabelBySearchTerm(data); + if (!this.loading) this.fetchLabelBySearchTerm(data); }, DEBOUNCE_DELAY), }, }; @@ -100,7 +115,7 @@ export default { > <template #view-token="{ inputValue, cssClasses, listeners }"> <gl-token variant="search-value" :class="cssClasses" :style="containerStyle" v-on="listeners" - >~{{ activeLabel ? activeLabel.title : inputValue }}</gl-token + >~{{ activeLabel ? getLabelName(activeLabel) : inputValue }}</gl-token > </template> <template #suggestions> @@ -114,13 +129,17 @@ export default { <gl-dropdown-divider v-if="defaultLabels.length" /> <gl-loading-icon v-if="loading" /> <template v-else> - <gl-filtered-search-suggestion v-for="label in labels" :key="label.id" :value="label.title"> - <div class="gl-display-flex"> + <gl-filtered-search-suggestion + v-for="label in labels" + :key="label.id" + :value="getLabelName(label)" + > + <div class="gl-display-flex gl-align-items-center"> <span :style="{ backgroundColor: label.color }" class="gl-display-inline-block mr-2 p-2" ></span> - <div>{{ label.title }}</div> + <div>{{ getLabelName(label) }}</div> </div> </gl-filtered-search-suggestion> </template> diff --git a/app/assets/javascripts/vue_shared/components/modal_copy_button.vue b/app/assets/javascripts/vue_shared/components/modal_copy_button.vue index 7b36d57dfbf..38afd56bae6 100644 --- a/app/assets/javascripts/vue_shared/components/modal_copy_button.vue +++ b/app/assets/javascripts/vue_shared/components/modal_copy_button.vue @@ -101,6 +101,7 @@ export default { :data-clipboard-target="target" :data-clipboard-text="text" :title="title" + :aria-label="title" :category="category" icon="copy-to-clipboard" /> diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 834d28f1e3c..133c6217bc0 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -316,6 +316,7 @@ class MergeRequest < ApplicationRecord } scope :with_csv_entity_associations, -> { preload(:assignees, :approved_by_users, :author, :milestone, metrics: [:merged_by]) } + scope :with_jira_integration_associations, -> { preload(:metrics, :assignees, :author, :target_project, :source_project) } scope :by_target_branch_wildcard, ->(wildcard_branch_name) do where("target_branch LIKE ?", ApplicationRecord.sanitize_sql_like(wildcard_branch_name).tr('*', '%')) diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 13ab542b5d9..7b7c2ea9d7b 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -343,6 +343,10 @@ class Namespace < ApplicationRecord Plan.default end + def paid? + root? && actual_plan.paid? + end + def actual_limits # We default to PlanLimits.new otherwise a lot of specs would fail # On production each plan should already have associated limits record diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb index 85887e64a8b..9a2e29a6ee3 100644 --- a/app/serializers/pipeline_serializer.rb +++ b/app/serializers/pipeline_serializer.rb @@ -41,7 +41,6 @@ class PipelineSerializer < BaseSerializer def preloaded_relations [ :cancelable_statuses, - :latest_statuses_ordered_by_stage, :retryable_builds, :stages, :latest_statuses, diff --git a/app/views/groups/settings/_permanent_deletion.html.haml b/app/views/groups/settings/_permanent_deletion.html.haml index 8bd47fbea44..125a20060ed 100644 --- a/app/views/groups/settings/_permanent_deletion.html.haml +++ b/app/views/groups/settings/_permanent_deletion.html.haml @@ -5,4 +5,5 @@ = _('Removing this group also removes all child projects, including archived projects, and their resources.') %br %strong= _('Removed group can not be restored!') - = button_to _('Remove group'), '#', class: 'btn gl-button btn-danger js-confirm-danger', data: { 'confirm-danger-message' => remove_group_message(group) } + + = render 'groups/settings/remove_button', group: group diff --git a/app/views/groups/settings/_remove_button.html.haml b/app/views/groups/settings/_remove_button.html.haml new file mode 100644 index 00000000000..a04dba68b92 --- /dev/null +++ b/app/views/groups/settings/_remove_button.html.haml @@ -0,0 +1,7 @@ +- if group.paid? + .gl-alert.gl-alert-info.gl-mb-5{ data: { testid: 'group-has-linked-subscription-alert' } } + = sprite_icon('information-o', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title') + .gl-alert-body + = html_escape(_("This group can't be removed because it is linked to a subscription. To remove this group, %{linkStart}link the subscription%{linkEnd} with a different group.")) % { linkStart: "<a href=\"#{help_page_path('subscriptions/index', anchor: 'change-the-linked-namespace')}\">".html_safe, linkEnd: '</a>'.html_safe } + += button_to _('Remove group'), '#', class: ['btn gl-button btn-danger js-confirm-danger', ('disabled' if group.paid?)], data: { 'confirm-danger-message' => remove_group_message(group), 'testid' => 'remove-group-button' } diff --git a/app/views/layouts/nav/_breadcrumbs.html.haml b/app/views/layouts/nav/_breadcrumbs.html.haml index aeeffb6f4b6..81373a81f80 100644 --- a/app/views/layouts/nav/_breadcrumbs.html.haml +++ b/app/views/layouts/nav/_breadcrumbs.html.haml @@ -9,7 +9,7 @@ = button_tag class: 'toggle-mobile-nav', type: 'button' do %span.sr-only= _("Open sidebar") = sprite_icon('hamburger', size: 18) - .breadcrumbs-links.js-title-container{ data: { qa_selector: 'breadcrumb_links_content' } } + .breadcrumbs-links{ data: { testid: 'breadcrumb-links', qa_selector: 'breadcrumb_links_content' } } %ul.list-unstyled.breadcrumbs-list.js-breadcrumbs-list - unless hide_top_links = header_title diff --git a/changelogs/unreleased/321788-drop-unused-preload.yml b/changelogs/unreleased/321788-drop-unused-preload.yml new file mode 100644 index 00000000000..9d3590ecf42 --- /dev/null +++ b/changelogs/unreleased/321788-drop-unused-preload.yml @@ -0,0 +1,5 @@ +--- +title: Drop unused preload from PipelineSerializer +merge_request: 56988 +author: +type: performance diff --git a/changelogs/unreleased/325429-container-registry-cleanup-policy-wiped-all-images.yml b/changelogs/unreleased/325429-container-registry-cleanup-policy-wiped-all-images.yml new file mode 100644 index 00000000000..da4ed507f23 --- /dev/null +++ b/changelogs/unreleased/325429-container-registry-cleanup-policy-wiped-all-images.yml @@ -0,0 +1,5 @@ +--- +title: Always save default on empty values in Exp Policies +merge_request: 57470 +author: +type: fixed diff --git a/changelogs/unreleased/325812-update-the-package-settings-to-use-the-blue-primary-button.yml b/changelogs/unreleased/325812-update-the-package-settings-to-use-the-blue-primary-button.yml new file mode 100644 index 00000000000..197648875da --- /dev/null +++ b/changelogs/unreleased/325812-update-the-package-settings-to-use-the-blue-primary-button.yml @@ -0,0 +1,5 @@ +--- +title: Update the Package settings to use the blue primary button +merge_request: 57468 +author: +type: fixed diff --git a/changelogs/unreleased/cngo-add-aria-labels-to-icon-buttons.yml b/changelogs/unreleased/cngo-add-aria-labels-to-icon-buttons.yml new file mode 100644 index 00000000000..98bc341d16f --- /dev/null +++ b/changelogs/unreleased/cngo-add-aria-labels-to-icon-buttons.yml @@ -0,0 +1,5 @@ +--- +title: Add aria labels to icon buttons +merge_request: 57261 +author: +type: fixed diff --git a/changelogs/unreleased/id-n-1-for-jira-pulls.yml b/changelogs/unreleased/id-n-1-for-jira-pulls.yml new file mode 100644 index 00000000000..e6d37b54c49 --- /dev/null +++ b/changelogs/unreleased/id-n-1-for-jira-pulls.yml @@ -0,0 +1,5 @@ +--- +title: Resolve N + 1 for JIRA pulls +merge_request: 57482 +author: +type: performance diff --git a/config/feature_flags/development/usage_data_api.yml b/config/feature_flags/ops/usage_data_api.yml index 9ba8180eb5a..edb78c151d5 100644 --- a/config/feature_flags/development/usage_data_api.yml +++ b/config/feature_flags/ops/usage_data_api.yml @@ -3,6 +3,6 @@ name: usage_data_api introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41301 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/267114 milestone: '13.4' -type: development -group: group::product analytics +type: ops +group: group::product intelligence default_enabled: true diff --git a/doc/administration/geo/glossary.md b/doc/administration/geo/glossary.md index 98a29c27a89..1ec552326aa 100644 --- a/doc/administration/geo/glossary.md +++ b/doc/administration/geo/glossary.md @@ -6,7 +6,7 @@ type: howto --- -# Geo Glossary +# Geo Glossary **(PREMIUM SELF)** NOTE: We are updating the Geo documentation, user interface and commands to reflect these changes. Not all pages comply with diff --git a/doc/administration/geo/replication/version_specific_updates.md b/doc/administration/geo/replication/version_specific_updates.md index 883e5d44b69..4f0a4dc638c 100644 --- a/doc/administration/geo/replication/version_specific_updates.md +++ b/doc/administration/geo/replication/version_specific_updates.md @@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w type: howto --- -# Version-specific update instructions +# Version-specific update instructions **(PREMIUM SELF)** Review this page for update instructions for your version. These steps accompany the [general steps](updating_the_geo_nodes.md#general-update-steps) diff --git a/doc/administration/geo/setup/index.md b/doc/administration/geo/setup/index.md index 8308cbfa82e..5ec18e29f21 100644 --- a/doc/administration/geo/setup/index.md +++ b/doc/administration/geo/setup/index.md @@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w type: howto --- -# Setting up Geo +# Setting up Geo **(PREMIUM SELF)** These instructions assume you have a working instance of GitLab. They guide you through: diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 940f0c41c8c..1a2beeb40fb 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -4101,6 +4101,7 @@ finishes. > [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/19298) in GitLab 13.2. Use `release` to create a [release](../../user/project/releases/index.md). +Requires the `release-cli` to be available in your GitLab Runner Docker or shell executor. These keywords are supported: @@ -4122,6 +4123,69 @@ You must specify the Docker image to use for the `release-cli`: image: registry.gitlab.com/gitlab-org/release-cli:latest ``` +#### `release-cli` for shell executors + +> [Introduced](https://gitlab.com/gitlab-org/release-cli/-/issues/21) in GitLab 13.8. + +For GitLab Runner shell executors, you can download and install the `release-cli` manually for your [supported OS and architecture](https://release-cli-downloads.s3.amazonaws.com/latest/index.html). +Once installed, the `release` keyword should be available to you. + +**Install on Unix/Linux** + +1. Download the binary for your system, in the following example for amd64 systems: + + ```shell + curl --location --output /usr/local/bin/release-cli "https://release-cli-downloads.s3.amazonaws.com/latest/release-cli-linux-amd64" + ``` + +1. Give it permissions to execute: + + ```shell + sudo chmod +x /usr/local/bin/release-cli + ``` + +1. Verify `release-cli` is available: + + ```shell + $ release-cli -v + + release-cli version 0.6.0 + ``` + +**Install on Windows PowerShell** + +1. Create a folder somewhere in your system, for example `C:\GitLab\Release-CLI\bin` + + ```shell + New-Item -Path 'C:\GitLab\Release-CLI\bin' -ItemType Directory + ``` + +1. Download the executable file: + + ```shell + PS C:\> Invoke-WebRequest -Uri "https://release-cli-downloads.s3.amazonaws.com/latest/release-cli-windows-amd64.exe" -OutFile "C:\GitLab\Release-CLI\bin\release-cli.exe" + + Directory: C:\GitLab\Release-CLI + Mode LastWriteTime Length Name + ---- ------------- ------ ---- + d----- 3/16/2021 4:17 AM bin + + ``` + +1. Add the directory to your `$env:PATH`: + + ```shell + $env:PATH += ";C:\GitLab\Release-CLI\bin" + ``` + +1. Verify `release-cli` is available: + + ```shell + PS C:\> release-cli -v + + release-cli version 0.6.0 + ``` + #### `script` All jobs except [trigger](#trigger) jobs must have the `script` keyword. A `release` diff --git a/doc/development/usage_ping/dictionary.md b/doc/development/usage_ping/dictionary.md index 87ac8109d3c..8f0c961cdf6 100644 --- a/doc/development/usage_ping/dictionary.md +++ b/doc/development/usage_ping/dictionary.md @@ -9956,6 +9956,54 @@ Status: `implemented` Tiers: `premium`, `ultimate` +### `redis_hll_counters.epics_usage.g_project_management_users_setting_epic_due_date_as_fixed_monthly` + +Counts of MAU setting epic due date as inherited + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210325060507_g_project_management_users_setting_epic_due_date_as_fixed_monthly.yml) + +Group: `group::product planning` + +Status: `implemented` + +Tiers: `premium`, `ultimate` + +### `redis_hll_counters.epics_usage.g_project_management_users_setting_epic_due_date_as_fixed_weekly` + +Counts of WAU setting epic due date as fixed + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210325060623_g_project_management_users_setting_epic_due_date_as_fixed_weekly.yml) + +Group: `group::product planning` + +Status: `implemented` + +Tiers: `premium`, `ultimate` + +### `redis_hll_counters.epics_usage.g_project_management_users_setting_epic_due_date_as_inherited_monthly` + +Counts of MAU setting epic due date as inherited + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210325060315_g_project_management_users_setting_epic_due_date_as_inherited_monthly.yml) + +Group: `group::product planning` + +Status: `implemented` + +Tiers: `premium`, `ultimate` + +### `redis_hll_counters.epics_usage.g_project_management_users_setting_epic_due_date_as_inherited_weekly` + +Counts of WAU setting epic due date as inherited + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210325060903_g_project_management_users_setting_epic_due_date_as_inherited_weekly.yml) + +Group: `group::product planning` + +Status: `implemented` + +Tiers: `premium`, `ultimate` + ### `redis_hll_counters.epics_usage.g_project_management_users_setting_epic_start_date_as_fixed_monthly` Counts of MAU setting epic start date as fixed diff --git a/doc/user/project/issues/managing_issues.md b/doc/user/project/issues/managing_issues.md index be5075d8230..d1ce3a6877b 100644 --- a/doc/user/project/issues/managing_issues.md +++ b/doc/user/project/issues/managing_issues.md @@ -326,10 +326,11 @@ To enable it, you need to enable [ActionCable in-app mode](https://docs.gitlab.c ## Cached issue count **(FREE SELF)** > - [Introduced]([link-to-issue](https://gitlab.com/gitlab-org/gitlab/-/issues/243753)) in GitLab 13.9. -> - It's [deployed behind a feature flag](../../feature_flags.md), disabled by default. -> - It's disabled on GitLab.com. -> - It's not recommended for production use. -> - To use this feature in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-cached-issue-count) **(FREE SELF)** +> - It was [deployed behind a feature flag](../../feature_flags.md), disabled by default. +> - [Became enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/323493) in GitLab 13.10. +> - It's enabled on GitLab.com. +> - It's recommended for production use. +> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-cached-issue-count) **(FREE SELF)** WARNING: This feature might not be available to you. Check the **version history** note above for details. @@ -373,7 +374,7 @@ You can then see issue statuses in the issues list and the ## Enable or disable cached issue count **(FREE SELF)** -Cached issue count in the left sidebar is under development and not ready for production use. It is +Cached issue count in the left sidebar is under development but ready for production use. It is deployed behind a feature flag that is **enabled by default**. [GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md) can disable it. diff --git a/lib/api/usage_data.rb b/lib/api/usage_data.rb index c7d63f8d6ac..622637e4fc3 100644 --- a/lib/api/usage_data.rb +++ b/lib/api/usage_data.rb @@ -8,7 +8,7 @@ module API namespace 'usage_data' do before do - not_found! unless Feature.enabled?(:usage_data_api, default_enabled: true) + not_found! unless Feature.enabled?(:usage_data_api, default_enabled: :yaml, type: :ops) forbidden!('Invalid CSRF token is provided') unless verified_request? end diff --git a/lib/api/v3/github.rb b/lib/api/v3/github.rb index 3a0609651f4..7938339e1b1 100644 --- a/lib/api/v3/github.rb +++ b/lib/api/v3/github.rb @@ -75,11 +75,14 @@ module API # rubocop: enable CodeReuse/ActiveRecord def authorized_merge_requests - MergeRequestsFinder.new(current_user, authorized_only: !current_user.admin?).execute + MergeRequestsFinder.new(current_user, authorized_only: !current_user.admin?) + .execute.with_jira_integration_associations end def authorized_merge_requests_for_project(project) - MergeRequestsFinder.new(current_user, authorized_only: !current_user.admin?, project_id: project.id).execute + MergeRequestsFinder + .new(current_user, authorized_only: !current_user.admin?, project_id: project.id) + .execute.with_jira_integration_associations end # rubocop: disable CodeReuse/ActiveRecord diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index c7e215c143f..08c17058fcb 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -45,7 +45,7 @@ module Gitlab # Initialize gon.features with any flags that should be # made globally available to the frontend push_frontend_feature_flag(:snippets_binary_blob, default_enabled: false) - push_frontend_feature_flag(:usage_data_api, default_enabled: true) + push_frontend_feature_flag(:usage_data_api, type: :ops, default_enabled: :yaml) push_frontend_feature_flag(:security_auto_fix, default_enabled: false) end diff --git a/lib/gitlab/usage_data_counters/known_events/epic_events.yml b/lib/gitlab/usage_data_counters/known_events/epic_events.yml index 600c849db1f..2465dc0f1f1 100644 --- a/lib/gitlab/usage_data_counters/known_events/epic_events.yml +++ b/lib/gitlab/usage_data_counters/known_events/epic_events.yml @@ -51,6 +51,18 @@ aggregation: daily feature_flag: track_epics_activity +- name: g_project_management_users_setting_epic_due_date_as_fixed + category: epics_usage + redis_slot: project_management + aggregation: daily + feature_flag: track_epics_activity + +- name: g_project_management_users_setting_epic_due_date_as_inherited + category: epics_usage + redis_slot: project_management + aggregation: daily + feature_flag: track_epics_activity + - name: g_project_management_epic_issue_added category: epics_usage redis_slot: project_management diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 18f064a6bb8..ce9dadc16e8 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2917,6 +2917,9 @@ msgstr "" msgid "AlertSettings|Delete integration" msgstr "" +msgid "AlertSettings|Edit integration" +msgstr "" + msgid "AlertSettings|Edit payload" msgstr "" @@ -31061,6 +31064,9 @@ msgstr "" msgid "This group" msgstr "" +msgid "This group can't be removed because it is linked to a subscription. To remove this group, %{linkStart}link the subscription%{linkEnd} with a different group." +msgstr "" + msgid "This group cannot be invited to a project inside a group with enforced SSO" msgstr "" @@ -31070,6 +31076,9 @@ msgstr "" msgid "This group has been scheduled for permanent removal on %{date}" msgstr "" +msgid "This group is linked to a subscription" +msgstr "" + msgid "This group, including all subgroups, projects and git repositories, will be reachable from only the specified IP address ranges." msgstr "" diff --git a/spec/features/projects/settings/registry_settings_spec.rb b/spec/features/projects/settings/registry_settings_spec.rb index 6e4082d1391..bc60cdd2f8e 100644 --- a/spec/features/projects/settings/registry_settings_spec.rb +++ b/spec/features/projects/settings/registry_settings_spec.rb @@ -39,7 +39,7 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p select('7 days', from: 'Remove tags older than:') fill_in('Remove tags matching:', with: '.*-production') - submit_button = find('.btn.gl-button.btn-success') + submit_button = find('[data-testid="save-button"') expect(submit_button).not_to be_disabled submit_button.click end @@ -53,7 +53,7 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p within '#js-registry-policies' do fill_in('Remove tags matching:', with: '*-production') - submit_button = find('.btn.gl-button.btn-success') + submit_button = find('[data-testid="save-button"') expect(submit_button).not_to be_disabled submit_button.click end diff --git a/spec/features/projects/sub_group_issuables_spec.rb b/spec/features/projects/sub_group_issuables_spec.rb index 8c1d88276df..d7614201740 100644 --- a/spec/features/projects/sub_group_issuables_spec.rb +++ b/spec/features/projects/sub_group_issuables_spec.rb @@ -16,18 +16,18 @@ RSpec.describe 'Subgroup Issuables', :js do it 'shows the full subgroup title when issues index page is empty' do visit project_issues_path(project) - expect_to_have_full_subgroup_title + expect_to_have_breadcrumb_links end it 'shows the full subgroup title when merge requests index page is empty' do visit project_merge_requests_path(project) - expect_to_have_full_subgroup_title + expect_to_have_breadcrumb_links end - def expect_to_have_full_subgroup_title - title = find('.breadcrumbs-links') + def expect_to_have_breadcrumb_links + links = find('[data-testid="breadcrumb-links"]') - expect(title).to have_content 'group subgroup project' + expect(links).to have_content 'group subgroup project' end end diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js index f875e3bdb07..5523fe13cd6 100644 --- a/spec/frontend/api_spec.js +++ b/spec/frontend/api_spec.js @@ -264,18 +264,18 @@ describe('Api', () => { it('fetches group labels', (done) => { const options = { params: { search: 'foo' } }; const expectedGroup = 'gitlab-org'; - const expectedUrl = `${dummyUrlRoot}/groups/${expectedGroup}/-/labels`; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${expectedGroup}/labels`; mock.onGet(expectedUrl).reply(httpStatus.OK, [ { id: 1, - title: 'Foo Label', + name: 'Foo Label', }, ]); Api.groupLabels(expectedGroup, options) .then((res) => { expect(res.length).toBe(1); - expect(res[0].title).toBe('Foo Label'); + expect(res[0].name).toBe('Foo Label'); }) .then(done) .catch(done.fail); @@ -593,7 +593,7 @@ describe('Api', () => { }); describe('newLabel', () => { - it('creates a new label', (done) => { + it('creates a new project label', (done) => { const namespace = 'some namespace'; const project = 'some project'; const labelData = { some: 'data' }; @@ -618,26 +618,23 @@ describe('Api', () => { }); }); - it('creates a group label', (done) => { + it('creates a new group label', (done) => { const namespace = 'group/subgroup'; - const labelData = { some: 'data' }; + const labelData = { name: 'Foo', color: '#000000' }; const expectedUrl = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespace); - const expectedData = { - label: labelData, - }; mock.onPost(expectedUrl).reply((config) => { - expect(config.data).toBe(JSON.stringify(expectedData)); + expect(config.data).toBe(JSON.stringify({ color: labelData.color })); return [ httpStatus.OK, { - name: 'test', + ...labelData, }, ]; }); Api.newLabel(namespace, undefined, labelData, (response) => { - expect(response.name).toBe('test'); + expect(response.name).toBe('Foo'); done(); }); }); diff --git a/spec/frontend/design_management/components/toolbar/__snapshots__/design_navigation_spec.js.snap b/spec/frontend/design_management/components/toolbar/__snapshots__/design_navigation_spec.js.snap index 5eb86d4f9cb..3cb48d7632f 100644 --- a/spec/frontend/design_management/components/toolbar/__snapshots__/design_navigation_spec.js.snap +++ b/spec/frontend/design_management/components/toolbar/__snapshots__/design_navigation_spec.js.snap @@ -13,6 +13,7 @@ exports[`Design management pagination component renders navigation buttons 1`] = class="gl-mx-5" > <gl-button-stub + aria-label="Go to previous design" buttontextclasses="" category="primary" class="js-previous-design" @@ -24,6 +25,7 @@ exports[`Design management pagination component renders navigation buttons 1`] = /> <gl-button-stub + aria-label="Go to next design" buttontextclasses="" category="primary" class="js-next-design" diff --git a/spec/frontend/registry/settings/components/settings_form_spec.js b/spec/frontend/registry/settings/components/settings_form_spec.js index 7527910ad59..ad94da6ca66 100644 --- a/spec/frontend/registry/settings/components/settings_form_spec.js +++ b/spec/frontend/registry/settings/components/settings_form_spec.js @@ -77,33 +77,47 @@ describe('Settings Form', () => { }); }; - const mountComponentWithApollo = ({ provide = defaultProvidedValues, resolver } = {}) => { + const mountComponentWithApollo = ({ + provide = defaultProvidedValues, + mutationResolver, + queryPayload = expirationPolicyPayload(), + } = {}) => { localVue.use(VueApollo); const requestHandlers = [ - [updateContainerExpirationPolicyMutation, resolver], - [expirationPolicyQuery, jest.fn().mockResolvedValue(expirationPolicyPayload())], + [updateContainerExpirationPolicyMutation, mutationResolver], + [expirationPolicyQuery, jest.fn().mockResolvedValue(queryPayload)], ]; fakeApollo = createMockApollo(requestHandlers); + // This component does not do the query directly, but we need a proper cache to update fakeApollo.defaultClient.cache.writeQuery({ query: expirationPolicyQuery, variables: { projectPath: provide.projectPath, }, - ...expirationPolicyPayload(), + ...queryPayload, }); + // we keep in sync what prop we pass to the component with the cache + const { + data: { + project: { containerExpirationPolicy: value }, + }, + } = queryPayload; + mountComponent({ provide, + props: { + ...defaultProps, + value, + }, config: { localVue, apolloProvider: fakeApollo, }, }); - - return requestHandlers.map((resolvers) => resolvers[1]); }; beforeEach(() => { @@ -253,19 +267,44 @@ describe('Settings Form', () => { expect(findSaveButton().attributes('type')).toBe('submit'); }); - it('dispatches the correct apollo mutation', async () => { - const [expirationPolicyMutationResolver] = mountComponentWithApollo({ - resolver: jest.fn().mockResolvedValue(expirationPolicyMutationPayload()), + it('dispatches the correct apollo mutation', () => { + const mutationResolver = jest.fn().mockResolvedValue(expirationPolicyMutationPayload()); + mountComponentWithApollo({ + mutationResolver, }); findForm().trigger('submit'); - await expirationPolicyMutationResolver(); - expect(expirationPolicyMutationResolver).toHaveBeenCalled(); + + expect(mutationResolver).toHaveBeenCalled(); + }); + + it('saves the default values when a value is missing did not change the default options', async () => { + const mutationResolver = jest.fn().mockResolvedValue(expirationPolicyMutationPayload()); + mountComponentWithApollo({ + mutationResolver, + queryPayload: expirationPolicyPayload({ keepN: null, cadence: null, olderThan: null }), + }); + + await waitForPromises(); + + findForm().trigger('submit'); + + expect(mutationResolver).toHaveBeenCalledWith({ + input: { + cadence: 'EVERY_DAY', + enabled: true, + keepN: 'TEN_TAGS', + nameRegex: 'asdasdssssdfdf', + nameRegexKeep: 'sss', + olderThan: 'NINETY_DAYS', + projectPath: 'path', + }, + }); }); it('tracks the submit event', () => { mountComponentWithApollo({ - resolver: jest.fn().mockResolvedValue(expirationPolicyMutationPayload()), + mutationResolver: jest.fn().mockResolvedValue(expirationPolicyMutationPayload()), }); findForm().trigger('submit'); @@ -274,12 +313,12 @@ describe('Settings Form', () => { }); it('show a success toast when submit succeed', async () => { - const handlers = mountComponentWithApollo({ - resolver: jest.fn().mockResolvedValue(expirationPolicyMutationPayload()), + mountComponentWithApollo({ + mutationResolver: jest.fn().mockResolvedValue(expirationPolicyMutationPayload()), }); findForm().trigger('submit'); - await Promise.all(handlers); + await waitForPromises(); await wrapper.vm.$nextTick(); expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_SUCCESS_MESSAGE, { @@ -290,14 +329,14 @@ describe('Settings Form', () => { describe('when submit fails', () => { describe('user recoverable errors', () => { it('when there is an error is shown in a toast', async () => { - const handlers = mountComponentWithApollo({ - resolver: jest + mountComponentWithApollo({ + mutationResolver: jest .fn() .mockResolvedValue(expirationPolicyMutationPayload({ errors: ['foo'] })), }); findForm().trigger('submit'); - await Promise.all(handlers); + await waitForPromises(); await wrapper.vm.$nextTick(); expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('foo', { @@ -308,13 +347,12 @@ describe('Settings Form', () => { describe('global errors', () => { it('shows an error', async () => { - const handlers = mountComponentWithApollo({ - resolver: jest.fn().mockRejectedValue(expirationPolicyMutationPayload()), + mountComponentWithApollo({ + mutationResolver: jest.fn().mockRejectedValue(expirationPolicyMutationPayload()), }); findForm().trigger('submit'); - await Promise.all(handlers); - await wrapper.vm.$nextTick(); + await waitForPromises(); await wrapper.vm.$nextTick(); expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_ERROR_MESSAGE, { diff --git a/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap index 1bf757ea312..bab928318ce 100644 --- a/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap +++ b/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap @@ -40,6 +40,7 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = ` tag="div" > <gl-button-stub + aria-label="Copy URL" buttontextclasses="" category="primary" class="d-inline-flex" @@ -82,6 +83,7 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = ` tag="div" > <gl-button-stub + aria-label="Copy URL" buttontextclasses="" category="primary" class="d-inline-flex" diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js index 7676ce10ce0..8528c062426 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js @@ -118,6 +118,22 @@ describe('LabelToken', () => { wrapper = createComponent(); }); + describe('getLabelName', () => { + it('returns value of `name` or `title` property present in provided label param', () => { + let mockLabel = { + title: 'foo', + }; + + expect(wrapper.vm.getLabelName(mockLabel)).toBe(mockLabel.title); + + mockLabel = { + name: 'foo', + }; + + expect(wrapper.vm.getLabelName(mockLabel)).toBe(mockLabel.name); + }); + }); + describe('fetchLabelBySearchTerm', () => { it('calls `config.fetchLabels` with provided searchTerm param', () => { jest.spyOn(wrapper.vm.config, 'fetchLabels'); diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 2063de691a3..351486ae801 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -1415,6 +1415,12 @@ RSpec.describe Namespace do end end + describe '#paid?' do + it 'returns false for a root namespace with a free plan' do + expect(namespace.paid?).to eq(false) + end + end + describe '#shared_runners_setting' do using RSpec::Parameterized::TableSyntax diff --git a/spec/requests/api/v3/github_spec.rb b/spec/requests/api/v3/github_spec.rb index 197c6cbb0eb..521187f0a81 100644 --- a/spec/requests/api/v3/github_spec.rb +++ b/spec/requests/api/v3/github_spec.rb @@ -3,10 +3,10 @@ require 'spec_helper' RSpec.describe API::V3::Github do - let(:user) { create(:user) } - let(:unauthorized_user) { create(:user) } - let(:admin) { create(:user, :admin) } - let(:project) { create(:project, :repository, creator: user) } + let_it_be(:user) { create(:user) } + let_it_be(:unauthorized_user) { create(:user) } + let_it_be(:admin) { create(:user, :admin) } + let_it_be(:project) { create(:project, :repository, creator: user) } before do project.add_maintainer(user) @@ -210,14 +210,14 @@ RSpec.describe API::V3::Github do end describe 'repo pulls' do - let(:project2) { create(:project, :repository, creator: user) } - let(:assignee) { create(:user) } - let(:assignee2) { create(:user) } - let!(:merge_request) do + let_it_be(:project2) { create(:project, :repository, creator: user) } + let_it_be(:assignee) { create(:user) } + let_it_be(:assignee2) { create(:user) } + let_it_be(:merge_request) do create(:merge_request, source_project: project, target_project: project, author: user, assignees: [assignee]) end - let!(:merge_request_2) do + let_it_be(:merge_request_2) do create(:merge_request, source_project: project2, target_project: project2, author: user, assignees: [assignee, assignee2]) end @@ -225,26 +225,54 @@ RSpec.describe API::V3::Github do project2.add_maintainer(user) end + def perform_request + jira_get v3_api(route, user) + end + describe 'GET /-/jira/pulls' do + let(:route) { '/repos/-/jira/pulls' } + it 'returns an array of merge requests with github format' do - jira_get v3_api('/repos/-/jira/pulls', user) + perform_request expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_an(Array) expect(json_response.size).to eq(2) expect(response).to match_response_schema('entities/github/pull_requests') end + + it 'returns multiple merge requests without N + 1' do + perform_request + + control_count = ActiveRecord::QueryRecorder.new { perform_request }.count + + create(:merge_request, source_project: project, source_branch: 'fix') + + expect { perform_request }.not_to exceed_query_limit(control_count) + end end describe 'GET /repos/:namespace/:project/pulls' do + let(:route) { "/repos/#{project.namespace.path}/#{project.path}/pulls" } + it 'returns an array of merge requests for the proper project in github format' do - jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/pulls", user) + perform_request expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_an(Array) expect(json_response.size).to eq(1) expect(response).to match_response_schema('entities/github/pull_requests') end + + it 'returns multiple merge requests without N + 1' do + perform_request + + control_count = ActiveRecord::QueryRecorder.new { perform_request }.count + + create(:merge_request, source_project: project, source_branch: 'fix') + + expect { perform_request }.not_to exceed_query_limit(control_count) + end end describe 'GET /repos/:namespace/:project/pulls/:id' do diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb index bcaaa61eb04..7784f8f53ff 100644 --- a/spec/serializers/pipeline_serializer_spec.rb +++ b/spec/serializers/pipeline_serializer_spec.rb @@ -202,7 +202,7 @@ RSpec.describe PipelineSerializer do # Existing numbers are high and require performance optimization # Ongoing issue: # https://gitlab.com/gitlab-org/gitlab/-/issues/225156 - expected_queries = Gitlab.ee? ? 85 : 76 + expected_queries = Gitlab.ee? ? 82 : 76 expect(recorded.count).to be_within(2).of(expected_queries) expect(recorded.cached_count).to eq(0) diff --git a/spec/support/shared_examples/features/error_tracking_shared_example.rb b/spec/support/shared_examples/features/error_tracking_shared_example.rb index 92fc54ce0b0..1bdc5355408 100644 --- a/spec/support/shared_examples/features/error_tracking_shared_example.rb +++ b/spec/support/shared_examples/features/error_tracking_shared_example.rb @@ -2,7 +2,7 @@ RSpec.shared_examples 'error tracking index page' do it 'renders the error index page', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do - within('div.js-title-container') do + within('[data-testid="breadcrumb-links"]') do expect(page).to have_content(project.namespace.name) expect(page).to have_content(project.name) end diff --git a/spec/views/groups/settings/_remove.html.haml_spec.rb b/spec/views/groups/settings/_remove.html.haml_spec.rb new file mode 100644 index 00000000000..07fe900bc2d --- /dev/null +++ b/spec/views/groups/settings/_remove.html.haml_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'groups/settings/_remove.html.haml' do + describe 'render' do + it 'enables the Remove group button for a group' do + group = build(:group) + + render 'groups/settings/remove', group: group + + expect(rendered).to have_selector '[data-testid="remove-group-button"]' + expect(rendered).not_to have_selector '[data-testid="remove-group-button"].disabled' + expect(rendered).not_to have_selector '[data-testid="group-has-linked-subscription-alert"]' + end + end +end |