diff options
78 files changed, 671 insertions, 367 deletions
diff --git a/app/assets/javascripts/ci/runner/components/runner_platforms_radio_group.vue b/app/assets/javascripts/ci/runner/components/runner_platforms_radio_group.vue index 273226141d2..20e791aa3b3 100644 --- a/app/assets/javascripts/ci/runner/components/runner_platforms_radio_group.vue +++ b/app/assets/javascripts/ci/runner/components/runner_platforms_radio_group.vue @@ -1,5 +1,4 @@ <script> -import AWS_LOGO_URL from '@gitlab/svgs/dist/illustrations/logos/aws.svg?url'; import DOCKER_LOGO_URL from '@gitlab/svgs/dist/illustrations/third-party-logos/ci_cd-template-logos/docker.png'; import KUBERNETES_LOGO_URL from '@gitlab/svgs/dist/illustrations/logos/kubernetes.svg?url'; import { GlFormRadioGroup, GlIcon, GlLink } from '@gitlab/ui'; @@ -8,7 +7,6 @@ import { LINUX_PLATFORM, MACOS_PLATFORM, WINDOWS_PLATFORM, - AWS_PLATFORM, DOCKER_HELP_URL, KUBERNETES_HELP_URL, } from '../constants'; @@ -43,8 +41,6 @@ export default { MACOS_PLATFORM, WINDOWS_PLATFORM, - AWS_PLATFORM, - AWS_LOGO_URL, DOCKER_HELP_URL, DOCKER_LOGO_URL, KUBERNETES_HELP_URL, @@ -72,20 +68,6 @@ export default { </div> <div class="gl-mt-3 gl-mb-6"> - <label>{{ s__('Runners|Cloud templates') }}</label> - <!-- eslint-disable @gitlab/vue-require-i18n-strings --> - <div class="gl-display-flex gl-flex-wrap gl-gap-5"> - <runner-platforms-radio - v-model="model" - :image="$options.AWS_LOGO_URL" - :value="$options.AWS_PLATFORM" - > - AWS - </runner-platforms-radio> - </div> - </div> - - <div class="gl-mt-3 gl-mb-6"> <label>{{ s__('Runners|Containers') }}</label> <div class="gl-display-flex gl-flex-wrap gl-gap-5"> diff --git a/app/assets/javascripts/ci/runner/constants.js b/app/assets/javascripts/ci/runner/constants.js index 1225c0d7583..4e36a410a66 100644 --- a/app/assets/javascripts/ci/runner/constants.js +++ b/app/assets/javascripts/ci/runner/constants.js @@ -195,7 +195,6 @@ export const GROUP_FILTERED_SEARCH_NAMESPACE = 'group_runners'; export const LINUX_PLATFORM = 'linux'; export const MACOS_PLATFORM = 'osx'; export const WINDOWS_PLATFORM = 'windows'; -export const AWS_PLATFORM = 'aws'; export const DOWNLOAD_LOCATIONS = { [LINUX_PLATFORM]: [ diff --git a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js index 50fca995c81..4d3647cdf5c 100644 --- a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js +++ b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js @@ -101,8 +101,10 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => { ], }; - IssuableTokenKeys.tokenKeys.splice(3, 0, approvedToken.token); - IssuableTokenKeys.conditions.push(...approvedToken.conditions); + if (gon.features.mrApprovedFilter) { + IssuableTokenKeys.tokenKeys.splice(3, 0, approvedToken.token); + IssuableTokenKeys.conditions.push(...approvedToken.conditions); + } const approvedBy = { token: { @@ -149,7 +151,7 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => { ], }; - const tokenPosition = 4; + const tokenPosition = gon.features.mrApprovedFilter ? 4 : 3; IssuableTokenKeys.tokenKeys.splice(tokenPosition, 0, approvedBy.token); IssuableTokenKeys.tokenKeysWithAlternative.splice( tokenPosition, diff --git a/app/assets/javascripts/issuable/components/csv_import_export_buttons.vue b/app/assets/javascripts/issuable/components/csv_import_export_buttons.vue index 2cc01c302ec..b492194d1cf 100644 --- a/app/assets/javascripts/issuable/components/csv_import_export_buttons.vue +++ b/app/assets/javascripts/issuable/components/csv_import_export_buttons.vue @@ -1,12 +1,5 @@ <script> -import { - GlButtonGroup, - GlButton, - GlDropdown, - GlDropdownItem, - GlTooltipDirective, - GlModalDirective, -} from '@gitlab/ui'; +import { GlDropdownItem, GlModalDirective } from '@gitlab/ui'; import { TYPE_ISSUE } from '~/issues/constants'; import { __ } from '~/locale'; import CsvExportModal from './csv_export_modal.vue'; @@ -17,19 +10,13 @@ export default { exportAsCsvButtonText: __('Export as CSV'), importCsvText: __('Import CSV'), importFromJiraText: __('Import from Jira'), - importIssuesText: __('Import issues'), }, - name: 'CsvImportExportButtons', components: { - GlButtonGroup, - GlButton, - GlDropdown, GlDropdownItem, CsvExportModal, CsvImportModal, }, directives: { - GlTooltip: GlTooltipDirective, GlModal: GlModalDirective, }, inject: { @@ -42,18 +29,12 @@ export default { showImportButton: { default: false, }, - containerClass: { - default: '', - }, canEdit: { default: false, }, projectImportJiraPath: { default: null, }, - showLabel: { - default: false, - }, }, props: { exportCsvPath: { @@ -74,48 +55,30 @@ export default { importModalId() { return `${this.issuableType}-import-modal`; }, - importButtonTooltipText() { - return this.showLabel ? null : this.$options.i18n.importIssuesText; - }, - importButtonIcon() { - return this.showLabel ? null : 'import'; - }, }, }; </script> <template> - <div :class="containerClass"> - <gl-button-group class="gl-w-full"> - <gl-button - v-if="showExportButton" - v-gl-tooltip="$options.i18n.exportAsCsvButtonText" - v-gl-modal="exportModalId" - icon="export" - :aria-label="$options.i18n.exportAsCsvButtonText" - data-qa-selector="export_as_csv_button" - /> - <gl-dropdown - v-if="showImportButton" - v-gl-tooltip="importButtonTooltipText" - data-qa-selector="import_issues_dropdown" - :text="$options.i18n.importIssuesText" - :text-sr-only="!showLabel" - :icon="importButtonIcon" - class="gl-w-full gl-md-w-auto" - > - <gl-dropdown-item v-gl-modal="importModalId"> - {{ $options.i18n.importCsvText }} - </gl-dropdown-item> - <gl-dropdown-item - v-if="canEdit" - :href="projectImportJiraPath" - data-qa-selector="import_from_jira_link" - > - {{ $options.i18n.importFromJiraText }} - </gl-dropdown-item> - </gl-dropdown> - </gl-button-group> + <ul class="gl-display-contents"> + <gl-dropdown-item + v-if="showExportButton" + v-gl-modal="exportModalId" + data-qa-selector="export_as_csv_button" + > + {{ $options.i18n.exportAsCsvButtonText }} + </gl-dropdown-item> + <gl-dropdown-item v-if="showImportButton" v-gl-modal="importModalId"> + {{ $options.i18n.importCsvText }} + </gl-dropdown-item> + <gl-dropdown-item + v-if="showImportButton && canEdit" + :href="projectImportJiraPath" + data-qa-selector="import_from_jira_link" + > + {{ $options.i18n.importFromJiraText }} + </gl-dropdown-item> + <csv-export-modal v-if="showExportButton" :modal-id="exportModalId" @@ -123,5 +86,5 @@ export default { :issuable-count="issuableCount" /> <csv-import-modal v-if="showImportButton" :modal-id="importModalId" /> - </div> + </ul> </template> diff --git a/app/assets/javascripts/issuable/index.js b/app/assets/javascripts/issuable/index.js index ed336deb2ed..acc0161bf6a 100644 --- a/app/assets/javascripts/issuable/index.js +++ b/app/assets/javascripts/issuable/index.js @@ -36,11 +36,9 @@ export function initCsvImportExportButtons() { email, exportCsvPath, importCsvIssuesPath, - containerClass, canEdit, projectImportJiraPath, maxAttachmentSize, - showLabel, } = el.dataset; return new Vue({ @@ -52,11 +50,9 @@ export function initCsvImportExportButtons() { issuableType, email, importCsvIssuesPath, - containerClass, canEdit: parseBoolean(canEdit), projectImportJiraPath, maxAttachmentSize, - showLabel, }, render: (createElement) => createElement(CsvImportExportButtons, { diff --git a/app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue b/app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue index 652d4e0fb42..98429f3ffd1 100644 --- a/app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue +++ b/app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue @@ -1,5 +1,5 @@ <script> -import { GlButton, GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui'; +import { GlButton, GlDropdown, GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui'; import { helpPagePath } from '~/helpers/help_page_helper'; import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue'; import NewResourceDropdown from '~/vue_shared/components/new_resource_dropdown/new_resource_dropdown.vue'; @@ -12,6 +12,7 @@ export default { components: { CsvImportExportButtons, GlButton, + GlDropdown, GlEmptyState, GlLink, GlSprintf, @@ -71,12 +72,19 @@ export default { <gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm"> {{ $options.i18n.newIssueLabel }} </gl-button> - <csv-import-export-buttons + + <gl-dropdown v-if="showCsvButtons" class="gl-w-full gl-sm-w-auto gl-sm-mr-3" - :export-csv-path="exportCsvPathWithQuery" - :issuable-count="currentTabCount" - /> + :text="$options.i18n.importIssues" + data-qa-selector="import_issues_dropdown" + > + <csv-import-export-buttons + :export-csv-path="exportCsvPathWithQuery" + :issuable-count="currentTabCount" + /> + </gl-dropdown> + <new-resource-dropdown v-if="showNewIssueDropdown" class="gl-align-self-center" diff --git a/app/assets/javascripts/issues/list/components/issues_list_app.vue b/app/assets/javascripts/issues/list/components/issues_list_app.vue index 525daab41d5..7d077603530 100644 --- a/app/assets/javascripts/issues/list/components/issues_list_app.vue +++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue @@ -1,5 +1,12 @@ <script> -import { GlButton, GlFilteredSearchToken, GlTooltipDirective } from '@gitlab/ui'; +import { + GlButton, + GlFilteredSearchToken, + GlTooltipDirective, + GlDropdown, + GlDropdownItem, + GlDropdownDivider, +} from '@gitlab/ui'; import * as Sentry from '@sentry/browser'; import fuzzaldrinPlus from 'fuzzaldrin-plus'; import { isEmpty } from 'lodash'; @@ -9,8 +16,8 @@ import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.grap import getIssuesCountsQuery from 'ee_else_ce/issues/list/queries/get_issues_counts.query.graphql'; import { createAlert, VARIANT_INFO } from '~/alert'; import { TYPENAME_USER } from '~/graphql_shared/constants'; -import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils'; import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue'; +import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils'; import IssuableByEmail from '~/issuable/components/issuable_by_email.vue'; import { STATUS_ALL, @@ -114,6 +121,9 @@ export default { EmptyStateWithAnyIssues, EmptyStateWithoutAnyIssues, GlButton, + GlDropdown, + GlDropdownDivider, + GlDropdownItem, IssuableByEmail, IssuableList, IssueCardStatistics, @@ -802,26 +812,6 @@ export default { > <template #nav-actions> <gl-button - v-gl-tooltip - :href="rssPath" - icon="rss" - :title="$options.i18n.rssLabel" - :aria-label="$options.i18n.rssLabel" - /> - <gl-button - v-gl-tooltip - :href="calendarPath" - icon="calendar" - :title="$options.i18n.calendarLabel" - :aria-label="$options.i18n.calendarLabel" - /> - <csv-import-export-buttons - v-if="showCsvButtons" - class="gl-md-mr-3" - :export-csv-path="exportCsvPathWithQuery" - :issuable-count="currentTabCount" - /> - <gl-button v-if="canBulkUpdate" :disabled="isBulkEditButtonDisabled" @click="handleBulkUpdateClick" @@ -842,6 +832,30 @@ export default { :query-variables="newIssueDropdownQueryVariables" :extract-projects="extractProjects" /> + <gl-dropdown + v-gl-tooltip.hover="$options.i18n.actionsLabel" + category="tertiary" + icon="ellipsis_v" + no-caret + :text="$options.i18n.actionsLabel" + text-sr-only + data-qa-selector="issues_list_more_actions_dropdown" + > + <csv-import-export-buttons + v-if="showCsvButtons" + :export-csv-path="exportCsvPathWithQuery" + :issuable-count="currentTabCount" + /> + + <gl-dropdown-divider v-if="showCsvButtons" /> + + <gl-dropdown-item :href="rssPath"> + {{ $options.i18n.rssLabel }} + </gl-dropdown-item> + <gl-dropdown-item :href="calendarPath"> + {{ $options.i18n.calendarLabel }} + </gl-dropdown-item> + </gl-dropdown> </template> <template #timeframe="{ issuable = {} }"> diff --git a/app/assets/javascripts/issues/list/constants.js b/app/assets/javascripts/issues/list/constants.js index f998033b792..990ba1c0621 100644 --- a/app/assets/javascripts/issues/list/constants.js +++ b/app/assets/javascripts/issues/list/constants.js @@ -85,6 +85,7 @@ export const i18n = { editIssues: __('Edit issues'), errorFetchingCounts: __('An error occurred while getting issue counts'), errorFetchingIssues: __('An error occurred while loading issues'), + importIssues: __('Import issues'), issueRepositioningMessage: __( 'Issues are being rebalanced at the moment, so manual reordering is disabled.', ), diff --git a/app/assets/javascripts/issues/show/components/task_list_item_actions.vue b/app/assets/javascripts/issues/show/components/task_list_item_actions.vue index 03d298e0ddf..5160903c762 100644 --- a/app/assets/javascripts/issues/show/components/task_list_item_actions.vue +++ b/app/assets/javascripts/issues/show/components/task_list_item_actions.vue @@ -1,5 +1,5 @@ <script> -import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui'; import { __, s__ } from '~/locale'; import eventHub from '../event_hub'; @@ -10,38 +10,48 @@ export default { taskActions: s__('WorkItem|Task actions'), }, components: { - GlDropdown, - GlDropdownItem, + GlDisclosureDropdown, + GlDisclosureDropdownItem, }, inject: ['canUpdate'], methods: { convertToTask() { eventHub.$emit('convert-task-list-item', this.$el.closest('li').dataset.sourcepos); + this.closeDropdown(); }, deleteTaskListItem() { eventHub.$emit('delete-task-list-item', this.$el.closest('li').dataset.sourcepos); + this.closeDropdown(); + }, + closeDropdown() { + this.$refs.dropdown.close(); }, }, }; </script> <template> - <gl-dropdown + <gl-disclosure-dropdown + v-if="canUpdate" + ref="dropdown" class="task-list-item-actions-wrapper" category="tertiary" icon="ellipsis_v" - lazy no-caret - right - :text="$options.i18n.taskActions" + placement="right" + :toggle-text="$options.i18n.taskActions" text-sr-only - toggle-class="task-list-item-actions gl-opacity-0 gl-p-2!" + toggle-class="task-list-item-actions gl-opacity-0 gl-p-2! " > - <gl-dropdown-item v-if="canUpdate" @click="convertToTask"> - {{ $options.i18n.convertToTask }} - </gl-dropdown-item> - <gl-dropdown-item v-if="canUpdate" variant="danger" @click="deleteTaskListItem"> - {{ $options.i18n.delete }} - </gl-dropdown-item> - </gl-dropdown> + <gl-disclosure-dropdown-item class="gl-ml-2!" @action="convertToTask"> + <template #list-item> + {{ $options.i18n.convertToTask }} + </template> + </gl-disclosure-dropdown-item> + <gl-disclosure-dropdown-item class="gl-ml-2!" @action="deleteTaskListItem"> + <template #list-item> + <span class="gl-text-red-500!">{{ $options.i18n.delete }}</span> + </template> + </gl-disclosure-dropdown-item> + </gl-disclosure-dropdown> </template> diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/list.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/list.js index 983fc6d48aa..89cdbf6acba 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/list.js +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/list.js @@ -4,6 +4,7 @@ import { NAME_SORT_FIELD } from './common'; // Translations strings export const CONTAINER_REGISTRY_TITLE = s__('ContainerRegistry|Container Registry'); +export const SETTINGS_TEXT = s__('ContainerRegistry|Configure in settings'); export const CONNECTION_ERROR_TITLE = s__('ContainerRegistry|Docker connection error'); export const CONNECTION_ERROR_MESSAGE = s__( `ContainerRegistry|We are having trouble connecting to the Container Registry. Please try refreshing the page. If this error persists, please review %{docLinkStart}the troubleshooting documentation%{docLinkEnd}.`, diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js index a558550c91f..afddf78203d 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js @@ -36,6 +36,7 @@ export default () => { isGroupPage, isAdmin, showCleanupPolicyLink, + showContainerRegistrySettings, showUnfinishedTagCleanupCallout, connectionError, invalidPathError, @@ -69,6 +70,7 @@ export default () => { isGroupPage: parseBoolean(isGroupPage), isAdmin: parseBoolean(isAdmin), showCleanupPolicyLink: parseBoolean(showCleanupPolicyLink), + showContainerRegistrySettings: parseBoolean(showContainerRegistrySettings), showUnfinishedTagCleanupCallout: parseBoolean(showUnfinishedTagCleanupCallout), connectionError: parseBoolean(connectionError), invalidPathError: parseBoolean(invalidPathError), diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue index ca416f6fcea..fe29fa8fdd7 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue @@ -1,5 +1,6 @@ <script> import { + GlButton, GlEmptyState, GlTooltipDirective, GlSprintf, @@ -28,12 +29,14 @@ import { GRAPHQL_PAGE_SIZE, FETCH_IMAGES_LIST_ERROR_MESSAGE, SORT_FIELDS, + SETTINGS_TEXT, } from '../constants/index'; import getContainerRepositoriesDetails from '../graphql/queries/get_container_repositories_details.query.graphql'; export default { name: 'RegistryListPage', components: { + GlButton, GlEmptyState, ProjectEmptyState: () => import( @@ -75,6 +78,7 @@ export default { CONNECTION_ERROR_MESSAGE, EMPTY_RESULT_TITLE, EMPTY_RESULT_MESSAGE, + SETTINGS_TEXT, }, searchConfig: SORT_FIELDS, apollo: { @@ -306,6 +310,13 @@ export default { :docker-push-command="dockerPushCommand" :docker-login-command="dockerLoginCommand" /> + <gl-button + v-if="config.showContainerRegistrySettings" + v-gl-tooltip="$options.i18n.SETTINGS_TEXT" + icon="settings" + :href="config.cleanupPoliciesSettingsPath" + :aria-label="$options.i18n.SETTINGS_TEXT" + /> </template> </registry-header> <persisted-search diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue index 650fa798db6..367395f4446 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue @@ -33,6 +33,11 @@ export default { default: false, }, }, + data() { + return { + isUserAvatarListExpanded: false, + }; + }, computed: { approvers() { return this.approvalState.approvedBy?.nodes || []; @@ -120,6 +125,14 @@ export default { return gon.current_user_id; }, }, + methods: { + onUserAvatarListExpanded() { + this.isUserAvatarListExpanded = true; + }, + onUserAvatarListCollapsed() { + this.isUserAvatarListExpanded = false; + }, + }, }; </script> @@ -130,9 +143,12 @@ export default { <span v-if="approvalLeftMessage">{{ message }}</span> <span v-else class="gl-font-weight-bold">{{ message }}</span> <user-avatar-list - class="gl-display-inline-flex gl-vertical-align-middle" + class="gl-display-inline-block" + :class="{ 'gl-pt-1': isUserAvatarListExpanded }" :img-size="24" :items="approvers" + @expanded="onUserAvatarListExpanded" + @collapsed="onUserAvatarListCollapsed" /> </template> <template v-if="disableCommittersApproval && currentUserHasCommitted"> diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue index 167db3ce1f2..335f9ab1df4 100644 --- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue @@ -60,9 +60,11 @@ export default { methods: { expand() { this.isExpanded = true; + this.$emit('expanded'); }, collapse() { this.isExpanded = false; + this.$emit('collapsed'); }, }, }; diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index b003ca564f3..d70b2e57a95 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -12,6 +12,10 @@ class DashboardController < Dashboard::ApplicationController before_action :set_show_full_reference, only: [:issues, :merge_requests] before_action :check_filters_presence!, only: [:issues, :merge_requests] + before_action only: :merge_requests do + push_frontend_feature_flag(:mr_approved_filter, type: :ops) + end + respond_to :html feature_category :user_profile, [:activity] diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index b7578bcf465..d2f65104d86 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -38,6 +38,10 @@ class GroupsController < Groups::ApplicationController push_force_frontend_feature_flag(:work_items, group.work_items_feature_flag_enabled?) end + before_action only: :merge_requests do + push_frontend_feature_flag(:mr_approved_filter, type: :ops) + end + helper_method :captcha_required? skip_cross_project_access_check :index, :new, :create, :edit, :update, :destroy, :projects diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index ef944dad6e9..a5d3bc4ac4a 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -32,6 +32,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo before_action :authenticate_user!, only: [:assign_related_issues] before_action :check_user_can_push_to_source_branch!, only: [:rebase] + before_action only: :index do + push_frontend_feature_flag(:mr_approved_filter, type: :ops) + end + before_action only: [:show, :diffs] do push_frontend_feature_flag(:content_editor_on_issues, project&.group) push_force_frontend_feature_flag(:content_editor_on_issues, project&.content_editor_on_issues_feature_flag_enabled?) diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb index 0ce2d52f168..f1c5d5e08ad 100644 --- a/app/finders/merge_requests_finder.rb +++ b/app/finders/merge_requests_finder.rb @@ -187,7 +187,7 @@ class MergeRequestsFinder < IssuableFinder def by_approved(items) approved_param = Gitlab::Utils.to_boolean(params.fetch(:approved, nil)) - return items if approved_param.nil? + return items if approved_param.nil? || Feature.disabled?(:mr_approved_filter, type: :ops) if approved_param items.with_approvals diff --git a/app/graphql/resolvers/merge_requests_resolver.rb b/app/graphql/resolvers/merge_requests_resolver.rb index c725f165682..0cdff272ee5 100644 --- a/app/graphql/resolvers/merge_requests_resolver.rb +++ b/app/graphql/resolvers/merge_requests_resolver.rb @@ -57,7 +57,10 @@ module Resolvers argument :approved, GraphQL::Types::Boolean, required: false, - description: 'Limit results to approved merge requests.' + description: <<~DESC + Limit results to approved merge requests. + Available only when the feature flag `mr_approved_filter` is enabled. + DESC argument :created_after, Types::TimeType, required: false, diff --git a/app/views/admin/sessions/new.html.haml b/app/views/admin/sessions/new.html.haml index 04d99657cfd..e9442cf6b53 100644 --- a/app/views/admin/sessions/new.html.haml +++ b/app/views/admin/sessions/new.html.haml @@ -4,7 +4,7 @@ .row.justify-content-center .col-md-5.new-session-forms-container .login-page - #signin-container{ class: "#{'borderless' if Feature.enabled?(:restyle_login_page, @project)}" } + #signin-container{ class: ('borderless' if Feature.enabled?(:restyle_login_page, @project)) } - if any_form_based_providers_enabled? = render 'devise/shared/tabs_ldap', show_password_form: allow_admin_mode_password_authentication_for_web?, render_signup_link: false - else diff --git a/app/views/admin/sessions/two_factor.html.haml b/app/views/admin/sessions/two_factor.html.haml index 65455f2ccf0..d02090d4880 100644 --- a/app/views/admin/sessions/two_factor.html.haml +++ b/app/views/admin/sessions/two_factor.html.haml @@ -4,7 +4,7 @@ .row.justify-content-center .col-md-5.new-session-forms-container .login-page - #signin-container + #signin-container{ class: ('borderless' if Feature.enabled?(:restyle_login_page, @project)) } = render 'devise/shared/tab_single', tab_title: _('Enter Admin Mode') .tab-content .login-box.tab-pane.gl-p-5.active{ id: 'login-pane', role: 'tabpanel' } diff --git a/app/views/projects/issues/service_desk/_nav_btns.html.haml b/app/views/projects/issues/service_desk/_nav_btns.html.haml index 818de77dc89..9b16dbe761f 100644 --- a/app/views/projects/issues/service_desk/_nav_btns.html.haml +++ b/app/views/projects/issues/service_desk/_nav_btns.html.haml @@ -6,15 +6,29 @@ - notification_email = @current_user.present? ? @current_user.notification_email_or_default : nil .nav-controls.issues-nav-controls.gl-font-size-0 - - if show_feed_buttons - = render 'shared/issuable/feed_buttons' - - .js-csv-import-export-buttons{ data: { show_export_button: show_export_button.to_s, show_import_button: show_import_button.to_s, issuable_type: issuable_type, issuable_count: issuables_count_for_state(issuable_type.to_sym, params[:state]), email: notification_email, export_csv_path: export_csv_project_issues_path(@project, request.query_parameters), import_csv_issues_path: import_csv_namespace_project_issues_path, container_class: 'gl-mr-3', can_edit: can_edit.to_s, project_import_jira_path: project_import_jira_path(@project), max_attachment_size: number_to_human_size(Gitlab::CurrentSettings.max_attachment_size.megabytes) } } - - if @can_bulk_update = button_tag _("Edit issues"), class: "gl-button btn btn-default gl-mr-3 js-bulk-update-toggle" - if show_new_issue_link?(@project) = link_to _("New issue"), new_project_issue_path(@project, issue: { milestone_id: finder.milestones.first.try(:id) }), - class: "gl-button btn btn-confirm", + class: "gl-button btn btn-confirm gl-mr-3", id: "new_issue_link" + +.dropdown.gl-dropdown + = button_tag type: 'button', class: "btn dropdown-toggle btn-default btn-md gl-button gl-dropdown gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret has-tooltip gl-display-none! gl-md-display-inline-flex!", data: { toggle: 'dropdown', title: _('Actions') } do + = sprite_icon "ellipsis_v", size: 16, css_class: "dropdown-icon gl-icon" + %span.gl-sr-only + = _('Actions') + = button_tag type: 'button', class: "btn dropdown-toggle btn-default btn-md btn-block gl-button gl-dropdown-toggle gl-md-display-none!", data: { 'toggle' => 'dropdown' } do + %span.gl-dropdown-button-text= _('Actions') + = sprite_icon "chevron-down", size: 16, css_class: "dropdown-icon gl-icon" + .dropdown-menu.dropdown-menu-right + .gl-dropdown-inner + .gl-dropdown-contents + %ul + .js-csv-import-export-buttons{ data: { show_export_button: show_export_button.to_s, show_import_button: show_import_button.to_s, issuable_type: issuable_type, issuable_count: issuables_count_for_state(issuable_type.to_sym, params[:state]), email: notification_email, export_csv_path: export_csv_project_issues_path(@project, request.query_parameters), import_csv_issues_path: import_csv_namespace_project_issues_path, can_edit: can_edit.to_s, project_import_jira_path: project_import_jira_path(@project), max_attachment_size: number_to_human_size(Gitlab::CurrentSettings.max_attachment_size.megabytes) } } + %li.gl-dropdown-divider + %hr.dropdown-divider + %li.gl-dropdown-item + - if show_feed_buttons + = render 'shared/issuable/feed_buttons' diff --git a/app/views/projects/merge_requests/_nav_btns.html.haml b/app/views/projects/merge_requests/_nav_btns.html.haml index beb6de4698c..6cb1eee01c4 100644 --- a/app/views/projects/merge_requests/_nav_btns.html.haml +++ b/app/views/projects/merge_requests/_nav_btns.html.haml @@ -1,12 +1,27 @@ - issuable_type = 'merge_request' - notification_email = @current_user.present? ? @current_user.notification_email_or_default : nil -= render 'shared/issuable/feed_buttons', show_calendar_button: false -.js-csv-import-export-buttons{ data: { show_export_button: "true", issuable_type: issuable_type, issuable_count: issuables_count_for_state(issuable_type.to_sym, params[:state]), email: notification_email, export_csv_path: export_csv_project_merge_requests_path(@project, request.query_parameters), container_class: 'gl-mr-3' } } - - if @can_bulk_update = render Pajamas::ButtonComponent.new(type: :submit, button_options: { class: 'gl-mr-3 js-bulk-update-toggle' }) do = _("Edit merge requests") - if merge_project = render Pajamas::ButtonComponent.new(href: new_merge_request_path, variant: :confirm) do = _("New merge request") + +.dropdown.gl-dropdown + = button_tag type: 'button', class: "btn dropdown-toggle btn-default btn-md gl-button gl-dropdown gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret has-tooltip gl-display-none! gl-md-display-inline-flex!", data: { toggle: 'dropdown', title: _('Actions') } do + = sprite_icon "ellipsis_v", size: 16, css_class: "dropdown-icon gl-icon" + %span.gl-sr-only + = _('Actions') + = button_tag type: 'button', class: "btn dropdown-toggle btn-default btn-md btn-block gl-button gl-dropdown-toggle gl-md-display-none!", data: { 'toggle' => 'dropdown' } do + %span.gl-dropdown-button-text= _('Actions') + = sprite_icon "chevron-down", size: 16, css_class: "dropdown-icon gl-icon" + .dropdown-menu.dropdown-menu-right + .gl-dropdown-inner + .gl-dropdown-contents + %ul + .js-csv-import-export-buttons{ data: { show_export_button: "true", issuable_type: issuable_type, issuable_count: issuables_count_for_state(issuable_type.to_sym, params[:state]), email: notification_email, export_csv_path: export_csv_project_merge_requests_path(@project, request.query_parameters) } } + %li.gl-dropdown-divider + %hr.dropdown-divider + %li.gl-dropdown-item + = render 'shared/issuable/feed_buttons', show_calendar_button: false diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml index 644aca2477b..e1ec510d271 100644 --- a/app/views/projects/registry/repositories/index.html.haml +++ b/app/views/projects/registry/repositories/index.html.haml @@ -18,6 +18,7 @@ "gid_prefix": container_repository_gid_prefix, "is_admin": current_user&.admin.to_s, "show_cleanup_policy_link": show_cleanup_policy_link(@project).to_s, + "show_container_registry_settings": show_container_registry_settings(@project).to_s, "cleanup_policies_settings_path": project_settings_packages_and_registries_path(@project), connection_error: (!!@connection_error).to_s, invalid_path_error: (!!@invalid_path_error).to_s, diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml index ad6e5578878..1ab9e288a9e 100644 --- a/app/views/shared/empty_states/_issues.html.haml +++ b/app/views/shared/empty_states/_issues.html.haml @@ -42,7 +42,7 @@ = link_to _('New issue'), button_path, class: 'gl-button btn btn-confirm', id: 'new_issue_link' - if show_import_button - .js-csv-import-export-buttons{ data: { show_import_button: 'true', issuable_type: 'issue', import_csv_issues_path: import_csv_namespace_project_issues_path, can_edit: can_edit.to_s, project_import_jira_path: project_import_jira_path(@project), max_attachment_size: number_to_human_size(Gitlab::CurrentSettings.max_attachment_size.megabytes), container_class: 'gl-w-full gl-sm-w-auto gl-sm-mr-3 gl-display-inline-flex gl-vertical-align-middle', show_label: 'true' } } + .js-csv-import-export-buttons{ data: { show_import_button: 'true', issuable_type: 'issue', import_csv_issues_path: import_csv_namespace_project_issues_path, can_edit: can_edit.to_s, project_import_jira_path: project_import_jira_path(@project), max_attachment_size: number_to_human_size(Gitlab::CurrentSettings.max_attachment_size.megabytes) } } %hr %p.gl-text-center.gl-mb-0 %strong diff --git a/app/views/shared/issuable/_feed_buttons.html.haml b/app/views/shared/issuable/_feed_buttons.html.haml index e0f676021a1..1a9bbac6f05 100644 --- a/app/views/shared/issuable/_feed_buttons.html.haml +++ b/app/views/shared/issuable/_feed_buttons.html.haml @@ -1,8 +1,10 @@ - show_calendar_button = local_assigns.fetch(:show_calendar_button, true) -= render Pajamas::ButtonComponent.new(href: safe_params.merge(rss_url_options), button_options: { class: 'has-tooltip btn-icon', title: _('Subscribe to RSS feed'), 'aria-label': _('Subscribe to RSS feed'), data: { container: 'body', testid: 'rss-feed-link' } }) do - = sprite_icon('rss') - - if show_calendar_button - = render Pajamas::ButtonComponent.new(href: safe_params.merge(calendar_url_options), button_options: { class: 'has-tooltip btn-icon', title: _('Subscribe to calendar'), 'aria-label': _('Subscribe to calendar'), data: { container: 'body' } }) do - = sprite_icon('calendar') + = link_to safe_params.merge(calendar_url_options), class: 'dropdown-item' do + .gl-dropdown-item-text-wrapper + = _("Subscribe to calendar") + += link_to safe_params.merge(rss_url_options), class: 'dropdown-item' do + .gl-dropdown-item-text-wrapper + = _("Subscribe to RSS feed") diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 06bc0ff5173..76678c48a86 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -162,14 +162,15 @@ %li.filter-dropdown-item{ data: { value: 'no', capitalize: true } } %button.gl-button.btn.btn-link{ type: 'button' } = _('No') - #js-dropdown-approved.filtered-search-input-dropdown-menu.dropdown-menu - %ul.filter-dropdown{ data: { dropdown: true } } - %li.filter-dropdown-item{ data: { value: 'yes', capitalize: true } } - %button.gl-button.btn.btn-link{ type: 'button' } - = _('Yes') - %li.filter-dropdown-item{ data: { value: 'no', capitalize: true } } - %button.gl-button.btn.btn-link{ type: 'button' } - = _('No') + - if ::Feature.enabled?(:mr_approved_filter, type: :ops) + #js-dropdown-approved.filtered-search-input-dropdown-menu.dropdown-menu + %ul.filter-dropdown{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'yes', capitalize: true } } + %button.gl-button.btn.btn-link{ type: 'button' } + = _('Yes') + %li.filter-dropdown-item{ data: { value: 'no', capitalize: true } } + %button.gl-button.btn.btn-link{ type: 'button' } + = _('No') #js-dropdown-confidential.filtered-search-input-dropdown-menu.dropdown-menu %ul.filter-dropdown{ data: { dropdown: true } } %li.filter-dropdown-item{ data: { value: 'yes', capitalize: true } } diff --git a/config/feature_flags/development/refactor_security_extension.yml b/config/feature_flags/development/refactor_security_extension.yml index 7651f7d387d..f7573acce23 100644 --- a/config/feature_flags/development/refactor_security_extension.yml +++ b/config/feature_flags/development/refactor_security_extension.yml @@ -1,8 +1,8 @@ --- name: refactor_security_extension -introduced_by_url: +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84896 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/365320 milestone: '14.10' type: development group: group::threat insights -default_enabled: false +default_enabled: true diff --git a/config/feature_flags/ops/mr_approved_filter.yml b/config/feature_flags/ops/mr_approved_filter.yml new file mode 100644 index 00000000000..e20c34d0126 --- /dev/null +++ b/config/feature_flags/ops/mr_approved_filter.yml @@ -0,0 +1,8 @@ +--- +name: mr_approved_filter +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118087 +rollout_issue_url: +milestone: '16.0' +type: ops +group: group::code review +default_enabled: false diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index b02d12bac8f..d0b19d20cde 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -15240,7 +15240,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | -| <a id="groupmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. | +| <a id="groupmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. | | <a id="groupmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. | | <a id="groupmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. | | <a id="groupmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. | @@ -16572,7 +16572,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | -| <a id="mergerequestassigneeassignedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. | +| <a id="mergerequestassigneeassignedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. | | <a id="mergerequestassigneeassignedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. | | <a id="mergerequestassigneeassignedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. | | <a id="mergerequestassigneeassignedmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. | @@ -16607,7 +16607,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | -| <a id="mergerequestassigneeauthoredmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. | +| <a id="mergerequestassigneeauthoredmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. | | <a id="mergerequestassigneeauthoredmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. | | <a id="mergerequestassigneeauthoredmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. | | <a id="mergerequestassigneeauthoredmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. | @@ -16659,7 +16659,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | -| <a id="mergerequestassigneereviewrequestedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. | +| <a id="mergerequestassigneereviewrequestedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. | | <a id="mergerequestassigneereviewrequestedmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. | | <a id="mergerequestassigneereviewrequestedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. | | <a id="mergerequestassigneereviewrequestedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. | @@ -16822,7 +16822,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | -| <a id="mergerequestauthorassignedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. | +| <a id="mergerequestauthorassignedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. | | <a id="mergerequestauthorassignedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. | | <a id="mergerequestauthorassignedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. | | <a id="mergerequestauthorassignedmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. | @@ -16857,7 +16857,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | -| <a id="mergerequestauthorauthoredmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. | +| <a id="mergerequestauthorauthoredmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. | | <a id="mergerequestauthorauthoredmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. | | <a id="mergerequestauthorauthoredmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. | | <a id="mergerequestauthorauthoredmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. | @@ -16909,7 +16909,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | -| <a id="mergerequestauthorreviewrequestedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. | +| <a id="mergerequestauthorreviewrequestedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. | | <a id="mergerequestauthorreviewrequestedmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. | | <a id="mergerequestauthorreviewrequestedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. | | <a id="mergerequestauthorreviewrequestedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. | @@ -17091,7 +17091,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | -| <a id="mergerequestparticipantassignedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. | +| <a id="mergerequestparticipantassignedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. | | <a id="mergerequestparticipantassignedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. | | <a id="mergerequestparticipantassignedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. | | <a id="mergerequestparticipantassignedmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. | @@ -17126,7 +17126,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | -| <a id="mergerequestparticipantauthoredmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. | +| <a id="mergerequestparticipantauthoredmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. | | <a id="mergerequestparticipantauthoredmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. | | <a id="mergerequestparticipantauthoredmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. | | <a id="mergerequestparticipantauthoredmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. | @@ -17178,7 +17178,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | -| <a id="mergerequestparticipantreviewrequestedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. | +| <a id="mergerequestparticipantreviewrequestedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. | | <a id="mergerequestparticipantreviewrequestedmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. | | <a id="mergerequestparticipantreviewrequestedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. | | <a id="mergerequestparticipantreviewrequestedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. | @@ -17360,7 +17360,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | -| <a id="mergerequestreviewerassignedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. | +| <a id="mergerequestreviewerassignedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. | | <a id="mergerequestreviewerassignedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. | | <a id="mergerequestreviewerassignedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. | | <a id="mergerequestreviewerassignedmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. | @@ -17395,7 +17395,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | -| <a id="mergerequestreviewerauthoredmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. | +| <a id="mergerequestreviewerauthoredmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. | | <a id="mergerequestreviewerauthoredmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. | | <a id="mergerequestreviewerauthoredmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. | | <a id="mergerequestreviewerauthoredmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. | @@ -17447,7 +17447,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | -| <a id="mergerequestreviewerreviewrequestedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. | +| <a id="mergerequestreviewerreviewrequestedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. | | <a id="mergerequestreviewerreviewrequestedmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. | | <a id="mergerequestreviewerreviewrequestedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. | | <a id="mergerequestreviewerreviewrequestedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. | @@ -19445,7 +19445,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | -| <a id="projectmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. | +| <a id="projectmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. | | <a id="projectmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. | | <a id="projectmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. | | <a id="projectmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. | @@ -21757,7 +21757,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | -| <a id="usercoreassignedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. | +| <a id="usercoreassignedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. | | <a id="usercoreassignedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. | | <a id="usercoreassignedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. | | <a id="usercoreassignedmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. | @@ -21792,7 +21792,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | -| <a id="usercoreauthoredmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. | +| <a id="usercoreauthoredmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. | | <a id="usercoreauthoredmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. | | <a id="usercoreauthoredmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. | | <a id="usercoreauthoredmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. | @@ -21844,7 +21844,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | -| <a id="usercorereviewrequestedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. | +| <a id="usercorereviewrequestedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. | | <a id="usercorereviewrequestedmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. | | <a id="usercorereviewrequestedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. | | <a id="usercorereviewrequestedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. | @@ -26440,7 +26440,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | -| <a id="userassignedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. | +| <a id="userassignedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. | | <a id="userassignedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. | | <a id="userassignedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. | | <a id="userassignedmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. | @@ -26475,7 +26475,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | -| <a id="userauthoredmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. | +| <a id="userauthoredmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. | | <a id="userauthoredmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. | | <a id="userauthoredmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. | | <a id="userauthoredmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. | @@ -26527,7 +26527,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | -| <a id="userreviewrequestedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. | +| <a id="userreviewrequestedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. | | <a id="userreviewrequestedmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. | | <a id="userreviewrequestedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. | | <a id="userreviewrequestedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. | diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index e806c44b07c..9e8f093144c 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -46,7 +46,7 @@ Supported attributes: | ------------------------------- | -------------- | -------- | ----------- | | `approved_by_ids` **(PREMIUM)** | integer array | **{dotted-circle}** No | Returns merge requests which have been approved by all the users with the given `id`. Maximum of 5. `None` returns merge requests with no approvals. `Any` returns merge requests with an approval. | | `approver_ids` **(PREMIUM)** | integer array | **{dotted-circle}** No | Returns merge requests which have specified all the users with the given `id` as individual approvers. `None` returns merge requests without approvers. `Any` returns merge requests with an approver. | -| `approved` | string | **{dotted-circle}** No | Filters merge requests by their `approved` status. `yes` returns only approved merge requests. `no` returns only non-approved merge requests. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3159) in GitLab 15.11. | +| `approved` | string | **{dotted-circle}** No | Filters merge requests by their `approved` status. `yes` returns only approved merge requests. `no` returns only non-approved merge requests. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3159) in GitLab 15.11. Available only when the feature flag `mr_approved_filter` is enabled. | | `assignee_id` | integer | **{dotted-circle}** No | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. | | `author_id` | integer | **{dotted-circle}** No | Returns merge requests created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. | | `author_username` | string | **{dotted-circle}** No | Returns merge requests created by the given `username`. Mutually exclusive with `author_id`. | @@ -250,7 +250,7 @@ Supported attributes: | `id` | integer or string | **{check-circle}** Yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) owned by the authenticated user. | | `approved_by_ids` **(PREMIUM)** | integer array | **{dotted-circle}** No | Returns merge requests which have been approved by all the users with the given `id`, with a maximum of 5. `None` returns merge requests with no approvals. `Any` returns merge requests with an approval. | | `approver_ids` **(PREMIUM)** | integer array | **{dotted-circle}** No | Returns merge requests which have specified all the users with the given `id` as individual approvers. `None` returns merge requests without approvers. `Any` returns merge requests with an approver. | -| `approved` | string | **{dotted-circle}** No | Filters merge requests by their `approved` status. `yes` returns only approved merge requests. `no` returns only non-approved merge requests. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3159) in GitLab 15.11. | +| `approved` | string | **{dotted-circle}** No | Filters merge requests by their `approved` status. `yes` returns only approved merge requests. `no` returns only non-approved merge requests. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3159) in GitLab 15.11. Available only when the feature flag `mr_approved_filter` is enabled. | | `assignee_id` | integer | **{dotted-circle}** No | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. | | `author_id` | integer | **{dotted-circle}** No | Returns merge requests created by the given user `id`. Mutually exclusive with `author_username`. | | `author_username` | string | **{dotted-circle}** No | Returns merge requests created by the given `username`. Mutually exclusive with `author_id`. | @@ -441,7 +441,7 @@ Supported attributes: | `approved_by_ids` **(PREMIUM)** | integer array | **{dotted-circle}** No | Returns merge requests which have been approved by all the users with the given `id`, with a maximum of 5. `None` returns merge requests with no approvals. `Any` returns merge requests with an approval. | | `approved_by_usernames` **(PREMIUM)** | string array | **{dotted-circle}** No | Returns merge requests which have been approved by all the users with the given `username`, with a maximum of 5. `None` returns merge requests with no approvals. `Any` returns merge requests with an approval. | | `approver_ids` **(PREMIUM)** | integer array | **{dotted-circle}** No | Returns merge requests which have specified all the users with the given `id`s as individual approvers. `None` returns merge requests without approvers. `Any` returns merge requests with an approver. | -| `approved` | string | **{dotted-circle}** No | Filters merge requests by their `approved` status. `yes` returns only approved merge requests. `no` returns only non-approved merge requests. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3159) in GitLab 15.11. | +| `approved` | string | **{dotted-circle}** No | Filters merge requests by their `approved` status. `yes` returns only approved merge requests. `no` returns only non-approved merge requests. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3159) in GitLab 15.11. Available only when the feature flag `mr_approved_filter` is enabled. | | `assignee_id` | integer | **{dotted-circle}** No | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. | | `author_id` | integer | **{dotted-circle}** No | Returns merge requests created by the given user `id`. Mutually exclusive with `author_username`. | | `author_username` | string | **{dotted-circle}** No | Returns merge requests created by the given `username`. Mutually exclusive with `author_id`. | diff --git a/doc/integration/jira/connect-app.md b/doc/integration/jira/connect-app.md index 7ab6eb1ab94..82dc32b1293 100644 --- a/doc/integration/jira/connect-app.md +++ b/doc/integration/jira/connect-app.md @@ -146,7 +146,7 @@ self-managed GitLab instances with Jira Cloud, you can do one of the following: [Prerequisites](#prerequisites-1) -To configure your Atlassian Cloud instance so you can install applications +To configure your Jira instance so you can install applications from outside the Marketplace: 1. Sign in to your Jira instance as an administrator. diff --git a/doc/user/project/import/jira.md b/doc/user/project/import/jira.md index c8b717d0421..e9c016c5d2b 100644 --- a/doc/user/project/import/jira.md +++ b/doc/user/project/import/jira.md @@ -59,7 +59,7 @@ Importing large projects may take several minutes depending on the size of the i To import Jira issues to a GitLab project: -1. On the **{issues}** **Issues** page, select **Import Issues** (**{import}**) **> Import from Jira**. +1. On the **{issues}** **Issues** page, select **Actions** (**{ellipsis_v}**) **> Import from Jira**. ![Import issues from Jira button](img/jira/import_issues_from_jira_button_v12_10.png) diff --git a/doc/user/project/issues/csv_import.md b/doc/user/project/issues/csv_import.md index 0dfdeb67f7a..c918793dcef 100644 --- a/doc/user/project/issues/csv_import.md +++ b/doc/user/project/issues/csv_import.md @@ -36,7 +36,7 @@ To import issues: 1. Go to your project's Issues list page. 1. Open the import feature, depending if the project has issues: - - The project has existing issues: in the upper-right corner, next to **Edit issues**, select the import icon (**{import}**). + - The project has existing issues: in the upper-right corner, next to **Edit issues**, select **Actions** (**{ellipsis_v}**) **> Import CSV**. - The project has no issues: in the middle of the page, select **Import CSV**. 1. Select the file you want to import, and then select **Import issues**. diff --git a/doc/user/project/issues/due_dates.md b/doc/user/project/issues/due_dates.md index f41c90a74a5..63b60e4538f 100644 --- a/doc/user/project/issues/due_dates.md +++ b/doc/user/project/issues/due_dates.md @@ -49,7 +49,8 @@ server's time zone. Issues with due dates can also be exported as an iCalendar feed. The URL of the feed can be added to calendar applications. The feed is accessible by selecting -the **Subscribe to calendar** button on the following pages: +the **Subscribe to calendar** option in the **Actions** (**{ellipsis_v}**) dropdown +list on the following pages: - The **Assigned Issues** page linked on the right side of the GitLab header - The **Project Issues** page diff --git a/doc/user/project/merge_requests/csv_export.md b/doc/user/project/merge_requests/csv_export.md index 9028a9411ea..f40b82a6280 100644 --- a/doc/user/project/merge_requests/csv_export.md +++ b/doc/user/project/merge_requests/csv_export.md @@ -16,7 +16,7 @@ To export merge requests to a CSV file: 1. On the left sidebar, select **Merge requests** . 1. Add any searches or filters. This can help you keep the size of the CSV file under the 15 MB limit. The limit ensures the file can be emailed to a variety of email providers. -1. Select **Export as CSV** (**{export}**). +1. Select **Actions** (**{ellipsis_v}**) **> Export as CSV**. 1. Confirm the correct number of merge requests are to be exported. 1. Select **Export merge requests**. diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 82e9a34fed3..4887fed6ca1 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -11478,6 +11478,9 @@ msgstr "" msgid "ContainerRegistry|Configuration digest: %{digest}" msgstr "" +msgid "ContainerRegistry|Configure in settings" +msgstr "" + msgid "ContainerRegistry|Container Registry" msgstr "" @@ -38210,9 +38213,6 @@ msgstr "" msgid "Runners|Clear selection" msgstr "" -msgid "Runners|Cloud templates" -msgstr "" - msgid "Runners|Command to register runner" msgstr "" @@ -52109,6 +52109,9 @@ msgstr "" msgid "ciReport|Container Scanning" msgstr "" +msgid "ciReport|Container Scanning detects known vulnerabilities in your container images." +msgstr "" + msgid "ciReport|Container scanning" msgstr "" @@ -52139,7 +52142,7 @@ msgstr "" msgid "ciReport|Dependency Scanning" msgstr "" -msgid "ciReport|Dependency Scanning detects known vulnerabilities in your source code's dependencies." +msgid "ciReport|Dependency Scanning detects known vulnerabilities in your project's dependencies." msgstr "" msgid "ciReport|Dependency scanning" @@ -52166,7 +52169,7 @@ msgstr "" msgid "ciReport|Dynamic Application Security Testing (DAST)" msgstr "" -msgid "ciReport|Dynamic Application Security Testing (DAST) detects known vulnerabilities in your web application." +msgid "ciReport|Dynamic Application Security Testing (DAST) detects vulnerabilities in your web application." msgstr "" msgid "ciReport|Failed to load %{reportName} report" @@ -52261,7 +52264,7 @@ msgstr "" msgid "ciReport|Secret Detection" msgstr "" -msgid "ciReport|Secret Detection detects secrets and credentials vulnerabilities in your source code." +msgid "ciReport|Secret Detection detects leaked credentials in your source code." msgstr "" msgid "ciReport|Secret detection" @@ -52294,7 +52297,7 @@ msgstr "" msgid "ciReport|Static Application Security Testing (SAST)" msgstr "" -msgid "ciReport|Static Application Security Testing (SAST) detects known vulnerabilities in your source code." +msgid "ciReport|Static Application Security Testing (SAST) detects potential vulnerabilities in your source code." msgstr "" msgid "ciReport|TTFB P90" diff --git a/qa/qa/page/project/issue/index.rb b/qa/qa/page/project/issue/index.rb index 7d162c6e48f..a4f4f6e3cf1 100644 --- a/qa/qa/page/project/issue/index.rb +++ b/qa/qa/page/project/issue/index.rb @@ -22,6 +22,13 @@ module QA view 'app/assets/javascripts/issuable/components/csv_import_export_buttons.vue' do element :export_as_csv_button element :import_from_jira_link + end + + view 'app/assets/javascripts/issues/list/components/issues_list_app.vue' do + element :issues_list_more_actions_dropdown + end + + view 'app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue' do element :import_issues_dropdown end @@ -59,6 +66,10 @@ module QA click_element(:import_issues_dropdown) end + def click_issues_list_more_actions_dropdown + click_element(:issues_list_more_actions_dropdown) + end + def export_issues_modal find_element(:export_issuable_modal) end diff --git a/qa/qa/specs/features/api/1_manage/import/import_github_repo_spec.rb b/qa/qa/specs/features/api/1_manage/import/import_github_repo_spec.rb index ed26061f3f8..85b62ec2ad1 100644 --- a/qa/qa/specs/features/api/1_manage/import/import_github_repo_spec.rb +++ b/qa/qa/specs/features/api/1_manage/import/import_github_repo_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Manage', product_group: :import, + RSpec.describe 'Manage', product_group: :import_and_integrate, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/407297', type: :investigating diff --git a/qa/qa/specs/features/api/1_manage/import/import_large_github_repo_spec.rb b/qa/qa/specs/features/api/1_manage/import/import_large_github_repo_spec.rb index 64ab8d8fc43..b81cb70eb18 100644 --- a/qa/qa/specs/features/api/1_manage/import/import_large_github_repo_spec.rb +++ b/qa/qa/specs/features/api/1_manage/import/import_large_github_repo_spec.rb @@ -7,7 +7,7 @@ require "etc" # rubocop:disable Rails/Pluck module QA RSpec.describe 'Manage', :github, requires_admin: 'creates users', only: { job: 'large-github-import' } do - describe 'Project import', product_group: :import do # rubocop:disable RSpec/MultipleMemoizedHelpers + describe 'Project import', product_group: :import_and_integrate do # rubocop:disable RSpec/MultipleMemoizedHelpers let(:github_repo) { ENV['QA_LARGE_IMPORT_REPO'] || 'rspec/rspec-core' } let(:import_max_duration) { ENV['QA_LARGE_IMPORT_DURATION']&.to_i || 7200 } let(:logger) { Runtime::Logger.logger } diff --git a/qa/qa/specs/features/api/1_manage/integrations/webhook_events_spec.rb b/qa/qa/specs/features/api/1_manage/integrations/webhook_events_spec.rb index b480a1f41cf..53c81b0e187 100644 --- a/qa/qa/specs/features/api/1_manage/integrations/webhook_events_spec.rb +++ b/qa/qa/specs/features/api/1_manage/integrations/webhook_events_spec.rb @@ -7,7 +7,7 @@ module QA :requires_admin, :integrations, :orchestrated, - product_group: :integrations + product_group: :import_and_integrate ) do before(:context) do toggle_local_requests(true) diff --git a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_group_spec.rb b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_group_spec.rb index 1f0c37df101..ac85795b2bb 100644 --- a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_group_spec.rb +++ b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_group_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe "Manage", :reliable, product_group: :import do + RSpec.describe "Manage", :reliable, product_group: :import_and_integrate do include_context "with gitlab group migration" describe "Gitlab migration" do diff --git a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_issue_spec.rb b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_issue_spec.rb index bc057f948a8..bff1837f51b 100644 --- a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_issue_spec.rb +++ b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_issue_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Manage' do - describe 'Gitlab migration', product_group: :import do + describe 'Gitlab migration', product_group: :import_and_integrate do include_context 'with gitlab project migration' let!(:source_issue) do diff --git a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb index d01adb5d5b4..ac8399b391d 100644 --- a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb +++ b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb @@ -5,7 +5,7 @@ # rubocop:disable Rails/Pluck, Layout/LineLength, RSpec/MultipleMemoizedHelpers module QA RSpec.describe "Manage", :skip_live_env, only: { job: "large-gitlab-import" } do - describe "Gitlab migration", orchestrated: false, product_group: :import do + describe "Gitlab migration", orchestrated: false, product_group: :import_and_integrate do include_context "with gitlab group migration" let!(:logger) { Runtime::Logger.logger } diff --git a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_members_spec.rb b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_members_spec.rb index 42793406e6c..ed79281b328 100644 --- a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_members_spec.rb +++ b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_members_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Manage' do - describe 'Gitlab migration', product_group: :import do + describe 'Gitlab migration', product_group: :import_and_integrate do include_context 'with gitlab project migration' let!(:source_member) do diff --git a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_mr_spec.rb b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_mr_spec.rb index 8c20c2cc0e2..7d785630a57 100644 --- a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_mr_spec.rb +++ b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_mr_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Manage' do - describe 'Gitlab migration', product_group: :import do + describe 'Gitlab migration', product_group: :import_and_integrate do include_context 'with gitlab project migration' let!(:source_project_with_readme) { true } diff --git a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_pipeline_spec.rb b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_pipeline_spec.rb index 8d631808d17..2f80f4c07ed 100644 --- a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_pipeline_spec.rb +++ b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_pipeline_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Manage' do - describe 'Gitlab migration', product_group: :import do + describe 'Gitlab migration', product_group: :import_and_integrate do include_context 'with gitlab project migration' context 'with ci pipeline' do diff --git a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_project_spec.rb b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_project_spec.rb index 040c92f296f..b74ac89917f 100644 --- a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_project_spec.rb +++ b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_project_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Manage' do - describe 'Gitlab migration', product_group: :import do + describe 'Gitlab migration', product_group: :import_and_integrate do include_context 'with gitlab project migration' # this spec is used as a sanity test for gitlab migration because it can run outside of orchestrated setup diff --git a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_release_spec.rb b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_release_spec.rb index b3510cef3e9..2443887c3d4 100644 --- a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_release_spec.rb +++ b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_release_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Manage' do - describe 'Gitlab migration', product_group: :import do + describe 'Gitlab migration', product_group: :import_and_integrate do include_context 'with gitlab project migration' context 'with release' do diff --git a/qa/qa/specs/features/api/1_manage/rate_limits_spec.rb b/qa/qa/specs/features/api/1_manage/rate_limits_spec.rb index 24088057abc..86fc154c1cf 100644 --- a/qa/qa/specs/features/api/1_manage/rate_limits_spec.rb +++ b/qa/qa/specs/features/api/1_manage/rate_limits_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Manage', :requires_admin, :skip_live_env, except: { job: 'review-qa-*' } do - describe 'rate limits', :reliable, product_group: :integrations do + describe 'rate limits', :reliable, product_group: :import_and_integrate do let(:rate_limited_user) { Resource::User.fabricate_via_api! } let(:api_client) { Runtime::API::Client.new(:gitlab, user: rate_limited_user) } let!(:request) { Runtime::API::Request.new(api_client, '/users') } diff --git a/qa/qa/specs/features/browser_ui/1_manage/import/import_github_repo_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/import/import_github_repo_spec.rb index d92d8de5567..94a3835b6c9 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/import/import_github_repo_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/import/import_github_repo_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Manage', product_group: :import do + RSpec.describe 'Manage', product_group: :import_and_integrate do describe 'GitHub import' do include_context 'with github import' diff --git a/qa/qa/specs/features/browser_ui/1_manage/integrations/jenkins/jenkins_build_status_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/integrations/jenkins/jenkins_build_status_spec.rb index b8d00c2faee..913317afc70 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/integrations/jenkins/jenkins_build_status_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/integrations/jenkins/jenkins_build_status_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Manage', :requires_admin, :skip_live_env, except: { job: 'review-qa-*' } do - describe 'Jenkins integration', product_group: :integrations do + describe 'Jenkins integration', product_group: :import_and_integrate do let(:jenkins_server) { Service::DockerRun::Jenkins.new } let(:jenkins_client) do diff --git a/qa/qa/specs/features/browser_ui/1_manage/integrations/jira/jira_basic_integration_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/integrations/jira/jira_basic_integration_spec.rb index 5a4031b4305..f6f4a6b3786 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/integrations/jira/jira_basic_integration_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/integrations/jira/jira_basic_integration_spec.rb @@ -4,7 +4,7 @@ module QA RSpec.describe 'Manage' do include Support::API - describe 'Jira integration', :jira, :orchestrated, :requires_admin, product_group: :integrations do + describe 'Jira integration', :jira, :orchestrated, :requires_admin, product_group: :import_and_integrate do let(:jira_project_key) { 'JITP' } let(:project) do Resource::Project.fabricate_via_api! do |project| diff --git a/qa/qa/specs/features/browser_ui/1_manage/integrations/jira/jira_issue_import_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/integrations/jira/jira_issue_import_spec.rb index f4042795995..02224406e6d 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/integrations/jira/jira_issue_import_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/integrations/jira/jira_issue_import_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Manage', :reliable do - describe 'Jira issue import', :jira, :orchestrated, :requires_admin, product_group: :integrations do + describe 'Jira issue import', :jira, :orchestrated, :requires_admin, product_group: :import_and_integrate do let(:jira_project_key) { "JITD" } let(:jira_issue_title) { "[#{jira_project_key}-1] Jira to GitLab Test Issue" } let(:jira_issue_description) { "This issue is for testing importing Jira issues to GitLab." } diff --git a/qa/qa/specs/features/browser_ui/1_manage/integrations/pipeline_status_emails_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/integrations/pipeline_status_emails_spec.rb index da49573b960..1ea19144a74 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/integrations/pipeline_status_emails_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/integrations/pipeline_status_emails_spec.rb @@ -24,7 +24,7 @@ module QA end end - RSpec.describe 'Manage', :orchestrated, :runner, :requires_admin, :smtp, product_group: :integrations do + RSpec.describe 'Manage', :orchestrated, :runner, :requires_admin, :smtp, product_group: :import_and_integrate do describe 'Pipeline status emails' do let(:executor) { "qa-runner-#{Time.now.to_i}" } let(:emails) { %w[foo@bar.com baz@buzz.com] } diff --git a/qa/qa/specs/features/browser_ui/1_manage/migration/gitlab_migration_group_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/migration/gitlab_migration_group_spec.rb index 4bcd2c44617..d8d8b66712a 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/migration/gitlab_migration_group_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/migration/gitlab_migration_group_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - describe 'Manage', :reliable, product_group: :import do + describe 'Manage', :reliable, product_group: :import_and_integrate do describe 'Gitlab migration' do include_context "with gitlab group migration" diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/export_as_csv_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/export_as_csv_spec.rb index 275f3a52f17..5e989cdb03f 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/export_as_csv_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/export_as_csv_spec.rb @@ -24,6 +24,8 @@ module QA it 'successfully exports issues list as CSV', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347968' do Page::Project::Issue::Index.perform do |index| + index.click_issues_list_more_actions_dropdown + index.click_export_as_csv_button expect(index.export_issues_modal).to have_content('2 issues selected') diff --git a/scripts/review_apps/base-config.yaml b/scripts/review_apps/base-config.yaml index 42552a5e9b1..940462e3805 100644 --- a/scripts/review_apps/base-config.yaml +++ b/scripts/review_apps/base-config.yaml @@ -229,7 +229,7 @@ postgresql: memory: 1000Mi limits: cpu: 1000m - memory: 1600Mi + memory: 1800Mi master: nodeSelector: preemptible: "false" diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb index 9f6fa146972..6d0d768d356 100644 --- a/spec/features/groups/issues_spec.rb +++ b/spec/features/groups/issues_spec.rb @@ -23,6 +23,10 @@ RSpec.describe 'Group issues page', feature_category: :subgroups do context 'rss feed' do let(:access_level) { ProjectFeature::ENABLED } + before do + click_button 'Actions' + end + context 'when signed in' do let(:user) do user_in_group.ensure_feed_token diff --git a/spec/features/issues/csv_spec.rb b/spec/features/issues/csv_spec.rb index 8629201459f..21d5041b210 100644 --- a/spec/features/issues/csv_spec.rb +++ b/spec/features/issues/csv_spec.rb @@ -16,6 +16,7 @@ RSpec.describe 'Issues csv', :js, feature_category: :team_planning do def request_csv(params = {}) visit project_issues_path(project, params) + click_button 'Actions' click_button 'Export as CSV' click_on 'Export issues' end diff --git a/spec/features/issues/rss_spec.rb b/spec/features/issues/rss_spec.rb index 75e7cd03a65..eb45d3c8d8b 100644 --- a/spec/features/issues/rss_spec.rb +++ b/spec/features/issues/rss_spec.rb @@ -23,6 +23,7 @@ RSpec.describe 'Project Issues RSS', :js, feature_category: :team_planning do before do sign_in(user) visit path + click_button 'Actions' end it_behaves_like "it has an RSS link with current_user's feed token" @@ -32,6 +33,7 @@ RSpec.describe 'Project Issues RSS', :js, feature_category: :team_planning do context 'when signed out' do before do visit path + click_button 'Actions' end it_behaves_like "it has an RSS link without a feed token" diff --git a/spec/features/merge_requests/rss_spec.rb b/spec/features/merge_requests/rss_spec.rb index 9c9f46278f6..5f697a4f79d 100644 --- a/spec/features/merge_requests/rss_spec.rb +++ b/spec/features/merge_requests/rss_spec.rb @@ -25,7 +25,7 @@ RSpec.describe 'Project Merge Requests RSS', feature_category: :code_review_work visit path end - it_behaves_like "it has an RSS button with current_user's feed token" + it_behaves_like "it has an RSS link with current_user's feed token" it_behaves_like "an autodiscoverable RSS feed with current_user's feed token" end @@ -34,7 +34,7 @@ RSpec.describe 'Project Merge Requests RSS', feature_category: :code_review_work visit path end - it_behaves_like "it has an RSS button without a feed token" + it_behaves_like "it has an RSS link without a feed token" it_behaves_like "an autodiscoverable RSS feed without a feed token" end diff --git a/spec/features/merge_requests/user_exports_as_csv_spec.rb b/spec/features/merge_requests/user_exports_as_csv_spec.rb index 23ac1b264ad..57d6ee69923 100644 --- a/spec/features/merge_requests/user_exports_as_csv_spec.rb +++ b/spec/features/merge_requests/user_exports_as_csv_spec.rb @@ -14,6 +14,7 @@ RSpec.describe 'Merge Requests > Exports as CSV', :js, feature_category: :code_r context 'button is clicked' do before do + click_button 'Actions' click_button 'Export as CSV' end diff --git a/spec/features/projects/container_registry_spec.rb b/spec/features/projects/container_registry_spec.rb index e8ac34d4355..5306a9f15c6 100644 --- a/spec/features/projects/container_registry_spec.rb +++ b/spec/features/projects/container_registry_spec.rb @@ -30,6 +30,20 @@ RSpec.describe 'Container Registry', :js, feature_category: :projects do expect(page).to have_title _('Container Registry') end + it 'does not have link to settings' do + visit_container_registry + + expect(page).not_to have_link _('Configure in settings') + end + + it 'has link to settings when user is maintainer' do + project.add_maintainer(user) + + visit_container_registry + + expect(page).to have_link _('Configure in settings') + end + context 'when there are no image repositories' do it 'list page has no container title' do visit_container_registry diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index aa167fe7aba..6d576bc8e38 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -498,16 +498,40 @@ RSpec.describe MergeRequestsFinder, feature_category: :code_review_workflow do create(:approval, merge_request: merge_request3, user: user2) end - it 'for approved' do - merge_requests = described_class.new(user, { approved: true }).execute + context 'when flag `mr_approved_filter` is disabled' do + before do + stub_feature_flags(mr_approved_filter: false) + end - expect(merge_requests).to contain_exactly(merge_request3) + it 'for approved' do + merge_requests = described_class.new(user, { approved: true }).execute + + expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request3, merge_request4, merge_request5) + end + + it 'for not approved' do + merge_requests = described_class.new(user, { approved: false }).execute + + expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request3, merge_request4, merge_request5) + end end - it 'for not approved' do - merge_requests = described_class.new(user, { approved: false }).execute + context 'when flag `mr_approved_filter` is enabled' do + before do + stub_feature_flags(mr_approved_filter: true) + end + + it 'for approved' do + merge_requests = described_class.new(user, { approved: true }).execute + + expect(merge_requests).to contain_exactly(merge_request3) + end + + it 'for not approved' do + merge_requests = described_class.new(user, { approved: false }).execute - expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request4, merge_request5) + expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request4, merge_request5) + end end end diff --git a/spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js b/spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js index d419b34df1b..6d11e5879f6 100644 --- a/spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js +++ b/spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js @@ -6,19 +6,12 @@ import { LINUX_PLATFORM, MACOS_PLATFORM, WINDOWS_PLATFORM, - AWS_PLATFORM, DOCKER_HELP_URL, KUBERNETES_HELP_URL, } from '~/ci/runner/constants'; import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue'; -const mockProvide = { - awsImgPath: 'awsLogo.svg', - dockerImgPath: 'dockerLogo.svg', - kubernetesImgPath: 'kubernetesLogo.svg', -}; - describe('RunnerPlatformsRadioGroup', () => { let wrapper; @@ -35,7 +28,6 @@ describe('RunnerPlatformsRadioGroup', () => { value: null, ...props, }, - provide: mockProvide, ...options, }); }; @@ -51,7 +43,6 @@ describe('RunnerPlatformsRadioGroup', () => { ['Linux', null], ['macOS', null], ['Windows', null], - ['AWS', expect.any(String)], ['Docker', expect.any(String)], ['Kubernetes', expect.any(String)], ]); @@ -69,7 +60,6 @@ describe('RunnerPlatformsRadioGroup', () => { ${'Linux'} | ${LINUX_PLATFORM} ${'macOS'} | ${MACOS_PLATFORM} ${'Windows'} | ${WINDOWS_PLATFORM} - ${'AWS'} | ${AWS_PLATFORM} `('user can select "$text"', async ({ text, value }) => { const radio = findFormRadioByText(text); expect(radio.props('value')).toBe(value); diff --git a/spec/frontend/issuable/components/csv_import_export_buttons_spec.js b/spec/frontend/issuable/components/csv_import_export_buttons_spec.js index a861148abb6..0e2f71fa3ee 100644 --- a/spec/frontend/issuable/components/csv_import_export_buttons_spec.js +++ b/spec/frontend/issuable/components/csv_import_export_buttons_spec.js @@ -1,5 +1,4 @@ -import { GlButton, GlDropdown } from '@gitlab/ui'; -import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; +import { createMockDirective } from 'helpers/vue_mock_directive'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import CsvExportModal from '~/issuable/components/csv_export_modal.vue'; import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue'; @@ -33,8 +32,7 @@ describe('CsvImportExportButtons', () => { }); } - const findExportCsvButton = () => wrapper.findComponent(GlButton); - const findImportDropdown = () => wrapper.findComponent(GlDropdown); + const findExportCsvButton = () => wrapper.findByRole('menuitem', { name: 'Export as CSV' }); const findImportCsvButton = () => wrapper.findByRole('menuitem', { name: 'Import CSV' }); const findImportFromJiraLink = () => wrapper.findByRole('menuitem', { name: 'Import from Jira' }); const findExportCsvModal = () => wrapper.findComponent(CsvExportModal); @@ -50,13 +48,6 @@ describe('CsvImportExportButtons', () => { expect(findExportCsvButton().exists()).toBe(true); }); - it('export button has a tooltip', () => { - const tooltip = getBinding(findExportCsvButton().element, 'gl-tooltip'); - - expect(tooltip).toBeDefined(); - expect(tooltip.value).toBe('Export as CSV'); - }); - it('renders the export modal', () => { expect(findExportCsvModal().props()).toMatchObject({ exportCsvPath, issuableCount }); }); @@ -64,7 +55,7 @@ describe('CsvImportExportButtons', () => { it('opens the export modal', () => { findExportCsvButton().trigger('click'); - expect(glModalDirective).toHaveBeenCalledWith(wrapper.vm.exportModalId); + expect(glModalDirective).toHaveBeenCalled(); }); }); @@ -83,79 +74,38 @@ describe('CsvImportExportButtons', () => { }); describe('when the showImportButton=true', () => { - beforeEach(() => { + it('renders the import csv menu item', () => { wrapper = createComponent({ showImportButton: true }); - }); - - it('displays the import dropdown', () => { - expect(findImportDropdown().exists()).toBe(true); - }); - it('renders the import csv menu item', () => { expect(findImportCsvButton().exists()).toBe(true); }); - describe('when showLabel=false', () => { - beforeEach(() => { - wrapper = createComponent({ showImportButton: true, showLabel: false }); - }); - - it('hides button text', () => { - expect(findImportDropdown().props()).toMatchObject({ - text: 'Import issues', - textSrOnly: true, - }); - }); - - it('import button has a tooltip', () => { - const tooltip = getBinding(findImportDropdown().element, 'gl-tooltip'); - - expect(tooltip).toBeDefined(); - expect(tooltip.value).toBe('Import issues'); - }); - }); - - describe('when showLabel=true', () => { - beforeEach(() => { - wrapper = createComponent({ showImportButton: true, showLabel: true }); - }); - - it('displays a button text', () => { - expect(findImportDropdown().props()).toMatchObject({ - text: 'Import issues', - textSrOnly: false, - }); - }); - - it('import button has no tooltip', () => { - const tooltip = getBinding(findImportDropdown().element, 'gl-tooltip'); - - expect(tooltip.value).toBe(null); - }); - }); - it('renders the import modal', () => { + wrapper = createComponent({ showImportButton: true }); + expect(findImportCsvModal().exists()).toBe(true); }); it('opens the import modal', () => { + wrapper = createComponent({ showImportButton: true }); + findImportCsvButton().trigger('click'); - expect(glModalDirective).toHaveBeenCalledWith(wrapper.vm.importModalId); + expect(glModalDirective).toHaveBeenCalled(); }); describe('import from jira link', () => { const projectImportJiraPath = 'gitlab-org/gitlab-test/-/import/jira'; - beforeEach(() => { - wrapper = createComponent({ - showImportButton: true, - canEdit: true, - projectImportJiraPath, + describe('when canEdit=true', () => { + beforeEach(() => { + wrapper = createComponent({ + showImportButton: true, + canEdit: true, + projectImportJiraPath, + }); }); - }); - describe('when canEdit=true', () => { it('renders the import dropdown item', () => { expect(findImportFromJiraLink().exists()).toBe(true); }); @@ -182,8 +132,8 @@ describe('CsvImportExportButtons', () => { wrapper = createComponent({ showImportButton: false }); }); - it('does not display the import dropdown', () => { - expect(findImportDropdown().exists()).toBe(false); + it('does not render the import csv menu item', () => { + expect(findImportCsvButton().exists()).toBe(false); }); it('does not render the import modal', () => { diff --git a/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js b/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js index 0a2e4e7c671..4ea3a39f15b 100644 --- a/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js +++ b/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js @@ -1,4 +1,4 @@ -import { GlEmptyState, GlLink } from '@gitlab/ui'; +import { GlDropdown, GlEmptyState, GlLink } from '@gitlab/ui'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue'; import EmptyStateWithoutAnyIssues from '~/issues/list/components/empty_state_without_any_issues.vue'; @@ -26,6 +26,7 @@ describe('EmptyStateWithoutAnyIssues component', () => { }; const findCsvImportExportButtons = () => wrapper.findComponent(CsvImportExportButtons); + const findCsvImportExportDropdown = () => wrapper.findComponent(GlDropdown); const findGlEmptyState = () => wrapper.findComponent(GlEmptyState); const findGlLink = () => wrapper.findComponent(GlLink); const findIssuesHelpPageLink = () => @@ -135,6 +136,7 @@ describe('EmptyStateWithoutAnyIssues component', () => { it('renders', () => { mountComponent({ props: { showCsvButtons: true } }); + expect(findCsvImportExportDropdown().props('text')).toBe('Import issues'); expect(findCsvImportExportButtons().props()).toMatchObject({ exportCsvPath: defaultProps.exportCsvPathWithQuery, issuableCount: 0, @@ -146,6 +148,7 @@ describe('EmptyStateWithoutAnyIssues component', () => { it('does not render', () => { mountComponent({ props: { showCsvButtons: false } }); + expect(findCsvImportExportDropdown().exists()).toBe(false); expect(findCsvImportExportButtons().exists()).toBe(false); }); }); diff --git a/spec/frontend/issues/list/components/issues_list_app_spec.js b/spec/frontend/issues/list/components/issues_list_app_spec.js index 61dbc9afeeb..d1b796c5aa6 100644 --- a/spec/frontend/issues/list/components/issues_list_app_spec.js +++ b/spec/frontend/issues/list/components/issues_list_app_spec.js @@ -1,4 +1,4 @@ -import { GlButton } from '@gitlab/ui'; +import { GlButton, GlDropdown } from '@gitlab/ui'; import * as Sentry from '@sentry/browser'; import { mount, shallowMount } from '@vue/test-utils'; import AxiosMockAdapter from 'axios-mock-adapter'; @@ -11,6 +11,7 @@ import getIssuesCountsQuery from 'ee_else_ce/issues/list/queries/get_issues_coun import createMockApollo from 'helpers/mock_apollo_helper'; import setWindowLocation from 'helpers/set_window_location_helper'; import { TEST_HOST } from 'helpers/test_constants'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { getIssuesCountsQueryResponse, @@ -126,12 +127,16 @@ describe('CE IssuesListApp component', () => { const mockIssuesQueryResponse = jest.fn().mockResolvedValue(defaultQueryResponse); const mockIssuesCountsQueryResponse = jest.fn().mockResolvedValue(getIssuesCountsQueryResponse); + const findCalendarButton = () => + wrapper.findByRole('menuitem', { name: IssuesListApp.i18n.calendarLabel }); const findCsvImportExportButtons = () => wrapper.findComponent(CsvImportExportButtons); + const findDropdown = () => wrapper.findComponent(GlDropdown); const findIssuableByEmail = () => wrapper.findComponent(IssuableByEmail); + const findGlButton = () => wrapper.findComponent(GlButton); const findGlButtons = () => wrapper.findAllComponents(GlButton); - const findGlButtonAt = (index) => findGlButtons().at(index); const findIssuableList = () => wrapper.findComponent(IssuableList); const findNewResourceDropdown = () => wrapper.findComponent(NewResourceDropdown); + const findRssButton = () => wrapper.findByRole('menuitem', { name: IssuesListApp.i18n.rssLabel }); const findLabelsToken = () => findIssuableList() @@ -232,63 +237,66 @@ describe('CE IssuesListApp component', () => { }); describe('header action buttons', () => { - it('renders rss button', async () => { - wrapper = mountComponent({ mountFn: mount }); - await waitForPromises(); + describe('actions dropdown', () => { + it('renders', () => { + wrapper = mountComponent({ mountFn: mount }); - expect(findGlButtonAt(0).props('icon')).toBe('rss'); - expect(findGlButtonAt(0).attributes()).toMatchObject({ - href: defaultProvide.rssPath, - 'aria-label': IssuesListApp.i18n.rssLabel, + expect(findDropdown().props()).toMatchObject({ + category: 'tertiary', + icon: 'ellipsis_v', + text: 'Actions', + textSrOnly: true, + }); }); - }); - it('renders calendar button', async () => { - wrapper = mountComponent({ mountFn: mount }); - await waitForPromises(); + describe('csv import/export buttons', () => { + describe('when user is signed in', () => { + beforeEach(() => { + setWindowLocation('?search=refactor&state=opened'); - expect(findGlButtonAt(1).props('icon')).toBe('calendar'); - expect(findGlButtonAt(1).attributes()).toMatchObject({ - href: defaultProvide.calendarPath, - 'aria-label': IssuesListApp.i18n.calendarLabel, - }); - }); + wrapper = mountComponent({ + provide: { initialSortBy: CREATED_DESC, isSignedIn: true }, + mountFn: mount, + }); - describe('csv import/export component', () => { - describe('when user is signed in', () => { - beforeEach(() => { - setWindowLocation('?search=refactor&state=opened'); + return waitForPromises(); + }); - wrapper = mountComponent({ - provide: { initialSortBy: CREATED_DESC, isSignedIn: true }, - mountFn: mount, + it('renders', () => { + expect(findCsvImportExportButtons().props()).toMatchObject({ + exportCsvPath: `${defaultProvide.exportCsvPath}?search=refactor&state=opened`, + issuableCount: 1, + }); }); + }); - return waitForPromises(); + describe('when user is not signed in', () => { + it('does not render', () => { + wrapper = mountComponent({ provide: { isSignedIn: false }, mountFn: mount }); + + expect(findCsvImportExportButtons().exists()).toBe(false); + }); }); - it('renders', () => { - expect(findCsvImportExportButtons().props()).toMatchObject({ - exportCsvPath: `${defaultProvide.exportCsvPath}?search=refactor&state=opened`, - issuableCount: 1, + describe('when in a group context', () => { + it('does not render', () => { + wrapper = mountComponent({ provide: { isProject: false }, mountFn: mount }); + + expect(findCsvImportExportButtons().exists()).toBe(false); }); }); }); - describe('when user is not signed in', () => { - it('does not render', () => { - wrapper = mountComponent({ provide: { isSignedIn: false }, mountFn: mount }); + it('renders RSS button link', () => { + wrapper = mountComponent({ mountFn: mountExtended }); - expect(findCsvImportExportButtons().exists()).toBe(false); - }); + expect(findRssButton().attributes('href')).toBe(defaultProvide.rssPath); }); - describe('when in a group context', () => { - it('does not render', () => { - wrapper = mountComponent({ provide: { isProject: false }, mountFn: mount }); + it('renders calendar button link', () => { + wrapper = mountComponent({ mountFn: mountExtended }); - expect(findCsvImportExportButtons().exists()).toBe(false); - }); + expect(findCalendarButton().attributes('href')).toBe(defaultProvide.calendarPath); }); }); @@ -296,7 +304,7 @@ describe('CE IssuesListApp component', () => { it('renders when user has permissions', () => { wrapper = mountComponent({ provide: { canBulkUpdate: true }, mountFn: mount }); - expect(findGlButtonAt(2).text()).toBe('Edit issues'); + expect(findGlButton().text()).toBe('Edit issues'); }); it('does not render when user does not have permissions', () => { @@ -309,7 +317,7 @@ describe('CE IssuesListApp component', () => { wrapper = mountComponent({ provide: { canBulkUpdate: true }, mountFn: mount }); jest.spyOn(eventHub, '$emit'); - findGlButtonAt(2).vm.$emit('click'); + findGlButton().vm.$emit('click'); await waitForPromises(); expect(eventHub.$emit).toHaveBeenCalledWith('issuables:enableBulkEdit'); @@ -320,8 +328,8 @@ describe('CE IssuesListApp component', () => { it('renders when user has permissions', () => { wrapper = mountComponent({ provide: { showNewIssueLink: true }, mountFn: mount }); - expect(findGlButtonAt(2).text()).toBe('New issue'); - expect(findGlButtonAt(2).attributes('href')).toBe(defaultProvide.newIssuePath); + expect(findGlButton().text()).toBe('New issue'); + expect(findGlButton().attributes('href')).toBe(defaultProvide.newIssuePath); }); it('does not render when user does not have permissions', () => { diff --git a/spec/frontend/issues/show/components/task_list_item_actions_spec.js b/spec/frontend/issues/show/components/task_list_item_actions_spec.js index 8caa5236796..7dacbefaeff 100644 --- a/spec/frontend/issues/show/components/task_list_item_actions_spec.js +++ b/spec/frontend/issues/show/components/task_list_item_actions_spec.js @@ -1,4 +1,4 @@ -import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import TaskListItemActions from '~/issues/show/components/task_list_item_actions.vue'; import eventHub from '~/issues/show/event_hub'; @@ -6,9 +6,9 @@ import eventHub from '~/issues/show/event_hub'; describe('TaskListItemActions component', () => { let wrapper; - const findGlDropdown = () => wrapper.findComponent(GlDropdown); - const findConvertToTaskItem = () => wrapper.findAllComponents(GlDropdownItem).at(0); - const findDeleteItem = () => wrapper.findAllComponents(GlDropdownItem).at(1); + const findGlDropdown = () => wrapper.findComponent(GlDisclosureDropdown); + const findConvertToTaskItem = () => wrapper.findAllComponents(GlDisclosureDropdownItem).at(0); + const findDeleteItem = () => wrapper.findAllComponents(GlDisclosureDropdownItem).at(1); const mountComponent = () => { const li = document.createElement('li'); @@ -20,6 +20,7 @@ describe('TaskListItemActions component', () => { provide: { canUpdate: true }, attachTo: document.querySelector('div'), }); + wrapper.vm.$refs.dropdown.close = jest.fn(); }; beforeEach(() => { @@ -30,8 +31,8 @@ describe('TaskListItemActions component', () => { expect(findGlDropdown().props()).toMatchObject({ category: 'tertiary', icon: 'ellipsis_v', - right: true, - text: TaskListItemActions.i18n.taskActions, + placement: 'right', + toggleText: TaskListItemActions.i18n.taskActions, textSrOnly: true, }); }); @@ -39,7 +40,7 @@ describe('TaskListItemActions component', () => { it('emits event when `Convert to task` dropdown item is clicked', () => { jest.spyOn(eventHub, '$emit'); - findConvertToTaskItem().vm.$emit('click'); + findConvertToTaskItem().vm.$emit('action'); expect(eventHub.$emit).toHaveBeenCalledWith('convert-task-list-item', '3:1-3:10'); }); @@ -47,7 +48,7 @@ describe('TaskListItemActions component', () => { it('emits event when `Delete` dropdown item is clicked', () => { jest.spyOn(eventHub, '$emit'); - findDeleteItem().vm.$emit('click'); + findDeleteItem().vm.$emit('action'); expect(eventHub.$emit).toHaveBeenCalledWith('delete-task-list-item', '3:1-3:10'); }); diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js index 5c353deacbb..1823bbfe533 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js +++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js @@ -1,8 +1,8 @@ -import { GlSkeletonLoader, GlSprintf, GlAlert } from '@gitlab/ui'; +import { GlSkeletonLoader, GlSprintf, GlAlert, GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; - import VueApollo from 'vue-apollo'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql'; @@ -16,6 +16,7 @@ import { DELETE_IMAGE_SUCCESS_MESSAGE, DELETE_IMAGE_ERROR_MESSAGE, SORT_FIELDS, + SETTINGS_TEXT, } from '~/packages_and_registries/container_registry/explorer/constants'; import deleteContainerRepositoryMutation from '~/packages_and_registries/container_registry/explorer/graphql/mutations/delete_container_repository.mutation.graphql'; import getContainerRepositoriesDetails from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repositories_details.query.graphql'; @@ -48,6 +49,7 @@ describe('List Page', () => { const findEmptyState = () => wrapper.findComponent(GlEmptyState); const findCliCommands = () => wrapper.findComponent(CliCommands); + const findSettingsLink = () => wrapper.findComponent(GlButton); const findProjectEmptyState = () => wrapper.findComponent(ProjectEmptyState); const findGroupEmptyState = () => wrapper.findComponent(GroupEmptyState); const findRegistryHeader = () => wrapper.findComponent(RegistryHeader); @@ -110,6 +112,9 @@ describe('List Page', () => { ...dockerCommands, }; }, + directives: { + GlTooltip: createMockDirective('gl-tooltip'), + }, }); }; @@ -122,6 +127,42 @@ describe('List Page', () => { expect(findRegistryHeader().props()).toMatchObject({ imagesCount: 2, metadataLoading: false, + helpPagePath: '', + hideExpirationPolicyData: false, + showCleanupPolicyLink: false, + expirationPolicy: {}, + cleanupPoliciesSettingsPath: '', + }); + }); + + describe('link to settings', () => { + beforeEach(() => { + const config = { + showContainerRegistrySettings: true, + cleanupPoliciesSettingsPath: 'bar', + }; + mountComponent({ config }); + }); + + it('is rendered', () => { + expect(findSettingsLink().exists()).toBe(true); + }); + + it('has the right icon', () => { + expect(findSettingsLink().props('icon')).toBe('settings'); + }); + + it('has the right attributes', () => { + expect(findSettingsLink().attributes()).toMatchObject({ + 'aria-label': SETTINGS_TEXT, + href: 'bar', + }); + }); + + it('sets tooltip with right label', () => { + const tooltip = getBinding(findSettingsLink().element, 'gl-tooltip'); + + expect(tooltip.value).toBe(SETTINGS_TEXT); }); }); @@ -235,6 +276,14 @@ describe('List Page', () => { expect(findCliCommands().exists()).toBe(false); }); + + it('link to settings is not visible', async () => { + mountComponent({ resolver, config }); + + await waitForApolloRequestRender(); + + expect(findSettingsLink().exists()).toBe(false); + }); }); }); diff --git a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js index 8c6b3cc464c..2c8d8b11b94 100644 --- a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js @@ -105,4 +105,31 @@ describe('MRWidget approvals summary', () => { expect(wrapper.findComponent(UserAvatarList).exists()).toBe(false); }); }); + + describe('user avatars list layout', () => { + beforeEach(() => { + createComponent(); + }); + + it('does not add top padding initially', () => { + const avatarsList = findAvatars(); + + expect(avatarsList.classes()).not.toContain('gl-pt-1'); + }); + + it('adds some top padding when the list is expanded', async () => { + const avatarsList = findAvatars(); + await avatarsList.vm.$emit('expanded'); + + expect(avatarsList.classes()).toContain('gl-pt-1'); + }); + + it('removes the top padding when the list collapsed', async () => { + const avatarsList = findAvatars(); + await avatarsList.vm.$emit('expanded'); + await avatarsList.vm.$emit('collapsed'); + + expect(avatarsList.classes()).not.toContain('gl-pt-1'); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js index 1754292cb63..075cb753301 100644 --- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js +++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js @@ -148,6 +148,13 @@ describe('UserAvatarList', () => { expect(links.length).toEqual(TEST_BREAKPOINT); }); + it('does not emit any event on mount', async () => { + factory(); + await nextTick(); + + expect(wrapper.emitted()).toEqual({}); + }); + describe('with expand clicked', () => { beforeEach(() => { factory(); @@ -160,13 +167,25 @@ describe('UserAvatarList', () => { expect(links.length).toEqual(props.items.length); }); - it('with collapse clicked, it renders avatars up to breakpoint', async () => { - clickButton(); + it('emits the `expanded` event', () => { + expect(wrapper.emitted('expanded')).toHaveLength(1); + }); - await nextTick(); - const links = wrapper.findAllComponents(UserAvatarLink); + describe('with collapse clicked', () => { + beforeEach(() => { + clickButton(); + }); + + it('renders avatars up to breakpoint', async () => { + await nextTick(); + const links = wrapper.findAllComponents(UserAvatarLink); + + expect(links.length).toEqual(TEST_BREAKPOINT); + }); - expect(links.length).toEqual(TEST_BREAKPOINT); + it('emits the `collapsed` event', () => { + expect(wrapper.emitted('collapsed')).toHaveLength(1); + }); }); }); }); diff --git a/spec/helpers/packages_helper_spec.rb b/spec/helpers/packages_helper_spec.rb index dcc5e336253..ae8a7f0c14c 100644 --- a/spec/helpers/packages_helper_spec.rb +++ b/spec/helpers/packages_helper_spec.rb @@ -129,6 +129,62 @@ RSpec.describe PackagesHelper, feature_category: :package_registry do end end + describe '#show_container_registry_settings' do + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + let_it_be(:admin) { create(:admin) } + + before do + allow(helper).to receive(:current_user) { user } + end + + subject { helper.show_container_registry_settings(project) } + + context 'with container registry config enabled' do + before do + stub_config(registry: { enabled: true }) + end + + context 'when user has permission' do + before do + allow(Ability).to receive(:allowed?).with(user, :admin_container_image, project).and_return(true) + end + + it { is_expected.to be(true) } + end + + context 'when user does not have permission' do + before do + allow(Ability).to receive(:allowed?).with(user, :admin_container_image, project).and_return(false) + end + + it { is_expected.to be(false) } + end + end + + context 'with container registry config disabled' do + before do + stub_config(registry: { enabled: false }) + end + + context 'when user has permission' do + before do + allow(Ability).to receive(:allowed?).with(user, :admin_container_image, project).and_return(true) + end + + it { is_expected.to be(false) } + end + + context 'when user does not have permission' do + before do + allow(Ability).to receive(:allowed?).with(user, :admin_container_image, project).and_return(false) + end + + it { is_expected.to be(false) } + end + end + end + describe '#show_group_package_registry_settings' do let_it_be(:group) { create(:group) } let_it_be(:user) { create(:user) } diff --git a/spec/models/protected_tag/create_access_level_spec.rb b/spec/models/protected_tag/create_access_level_spec.rb index 566f8695388..77a7d15b597 100644 --- a/spec/models/protected_tag/create_access_level_spec.rb +++ b/spec/models/protected_tag/create_access_level_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe ProtectedTag::CreateAccessLevel, feature_category: :source_code_management do + include_examples 'protected tag access' + describe 'associations' do it { is_expected.to belong_to(:deploy_key) } end @@ -10,16 +12,6 @@ RSpec.describe ProtectedTag::CreateAccessLevel, feature_category: :source_code_m describe 'validations', :aggregate_failures do let_it_be(:protected_tag) { create(:protected_tag) } - it 'verifies access levels' do - is_expected.to validate_inclusion_of(:access_level).in_array( - [ - Gitlab::Access::MAINTAINER, - Gitlab::Access::DEVELOPER, - Gitlab::Access::NO_ACCESS - ] - ) - end - context 'when deploy key enabled for the project' do let(:deploy_key) { create(:deploy_key, projects: [protected_tag.project]) } diff --git a/spec/support/shared_examples/features/rss_shared_examples.rb b/spec/support/shared_examples/features/rss_shared_examples.rb index 29ecbd0dc0e..f6566214e32 100644 --- a/spec/support/shared_examples/features/rss_shared_examples.rb +++ b/spec/support/shared_examples/features/rss_shared_examples.rb @@ -43,6 +43,7 @@ RSpec.shared_examples "updates atom feed link" do |type| it "for #{type}" do sign_in(user) visit path + click_button 'Actions', match: :first link = find_link('Subscribe to RSS feed') params = CGI.parse(URI.parse(link[:href]).query) diff --git a/spec/support/shared_examples/models/concerns/protected_ref_access_examples.rb b/spec/support/shared_examples/models/concerns/protected_ref_access_examples.rb new file mode 100644 index 00000000000..53ebc207128 --- /dev/null +++ b/spec/support/shared_examples/models/concerns/protected_ref_access_examples.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'protected ref access' do |association| + let_it_be(:project) { create(:project) } + let_it_be(:protected_ref) { create(association, project: project) } # rubocop:disable Rails/SaveBang + + it { is_expected.to validate_inclusion_of(:access_level).in_array(described_class.allowed_access_levels) } + + it { is_expected.to validate_presence_of(:access_level) } + + context 'when not role?' do + before do + allow(subject).to receive(:role?).and_return(false) + end + + it { is_expected.not_to validate_presence_of(:access_level) } + end + + describe '#check_access' do + let_it_be(:current_user) { create(:user) } + + let(:access_level) { ::Gitlab::Access::DEVELOPER } + + before_all do + project.add_maintainer(current_user) + end + + subject do + described_class.new( + association => protected_ref, + access_level: access_level + ) + end + + context 'when current_user is nil' do + it { expect(subject.check_access(nil)).to eq(false) } + end + + context 'when access_level is NO_ACCESS' do + let(:access_level) { ::Gitlab::Access::NO_ACCESS } + + it { expect(subject.check_access(current_user)).to eq(false) } + end + + context 'when current_user can push_code to project and access_level is permitted' do + before do + allow(current_user).to receive(:can?).with(:push_code, project).and_return(true) + end + + it { expect(subject.check_access(current_user)).to eq(true) } + end + + context 'when current_user cannot push_code to project' do + before do + allow(current_user).to receive(:can?).with(:push_code, project).and_return(false) + end + + it { expect(subject.check_access(current_user)).to eq(false) } + end + end +end diff --git a/spec/support/shared_examples/models/concerns/protected_tag_access_examples.rb b/spec/support/shared_examples/models/concerns/protected_tag_access_examples.rb new file mode 100644 index 00000000000..49f616d5a59 --- /dev/null +++ b/spec/support/shared_examples/models/concerns/protected_tag_access_examples.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'protected tag access' do + include_examples 'protected ref access', :protected_tag + + let_it_be(:protected_tag) { create(:protected_tag) } + + it { is_expected.to belong_to(:protected_tag) } + + describe '#project' do + before do + allow(protected_tag).to receive(:project) + end + + it 'delegates project to protected_tag association' do + described_class.new(protected_tag: protected_tag).project + + expect(protected_tag).to have_received(:project) + end + end +end |