diff options
49 files changed, 910 insertions, 356 deletions
diff --git a/.rubocop_todo/layout/first_hash_element_indentation.yml b/.rubocop_todo/layout/first_hash_element_indentation.yml index 83ce52272b7..05c0cfee47c 100644 --- a/.rubocop_todo/layout/first_hash_element_indentation.yml +++ b/.rubocop_todo/layout/first_hash_element_indentation.yml @@ -113,7 +113,6 @@ Layout/FirstHashElementIndentation: - 'ee/spec/services/ee/projects/unlink_fork_service_spec.rb' - 'ee/spec/services/external_status_checks/create_service_spec.rb' - 'ee/spec/services/external_status_checks/destroy_service_spec.rb' - - 'ee/spec/services/groups/create_service_spec.rb' - 'ee/spec/services/groups/destroy_service_spec.rb' - 'ee/spec/services/iterations/create_service_spec.rb' - 'ee/spec/services/projects/create_service_spec.rb' diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml index b8b267d6d86..1b241836da3 100644 --- a/.rubocop_todo/layout/line_length.yml +++ b/.rubocop_todo/layout/line_length.yml @@ -2117,7 +2117,6 @@ Layout/LineLength: - 'ee/spec/services/geo/hashed_storage_attachments_event_store_spec.rb' - 'ee/spec/services/gitlab_subscriptions/check_future_renewal_service_spec.rb' - 'ee/spec/services/gitlab_subscriptions/create_service_spec.rb' - - 'ee/spec/services/groups/create_service_spec.rb' - 'ee/spec/services/groups/memberships/export_service_spec.rb' - 'ee/spec/services/groups/transfer_service_spec.rb' - 'ee/spec/services/groups/update_repository_storage_service_spec.rb' @@ -4462,7 +4461,6 @@ Layout/LineLength: - 'spec/services/google_cloud/gcp_region_add_or_replace_service_spec.rb' - 'spec/services/google_cloud/service_accounts_service_spec.rb' - 'spec/services/groups/autocomplete_service_spec.rb' - - 'spec/services/groups/create_service_spec.rb' - 'spec/services/groups/transfer_service_spec.rb' - 'spec/services/groups/update_service_spec.rb' - 'spec/services/groups/update_shared_runners_service_spec.rb' diff --git a/.rubocop_todo/rspec/any_instance_of.yml b/.rubocop_todo/rspec/any_instance_of.yml index 9393170aa03..bac4094b61b 100644 --- a/.rubocop_todo/rspec/any_instance_of.yml +++ b/.rubocop_todo/rspec/any_instance_of.yml @@ -251,7 +251,6 @@ RSpec/AnyInstanceOf: - 'spec/services/events/render_service_spec.rb' - 'spec/services/git/branch_push_service_spec.rb' - 'spec/services/git/process_ref_changes_service_spec.rb' - - 'spec/services/groups/create_service_spec.rb' - 'spec/services/groups/update_service_spec.rb' - 'spec/services/issuable/destroy_service_spec.rb' - 'spec/services/issues/close_service_spec.rb' diff --git a/.rubocop_todo/rspec/context_wording.yml b/.rubocop_todo/rspec/context_wording.yml index a141ac1e508..2238a9b722c 100644 --- a/.rubocop_todo/rspec/context_wording.yml +++ b/.rubocop_todo/rspec/context_wording.yml @@ -715,7 +715,6 @@ RSpec/ContextWording: - 'ee/spec/services/gitlab_subscriptions/create_service_spec.rb' - 'ee/spec/services/gitlab_subscriptions/preview_billable_user_change_service_spec.rb' - 'ee/spec/services/group_saml/group_managed_accounts/transfer_membership_service_spec.rb' - - 'ee/spec/services/groups/create_service_spec.rb' - 'ee/spec/services/groups/destroy_service_spec.rb' - 'ee/spec/services/groups/mark_for_deletion_service_spec.rb' - 'ee/spec/services/groups/memberships/export_service_spec.rb' @@ -2562,7 +2561,6 @@ RSpec/ContextWording: - 'spec/services/git/wiki_push_service_spec.rb' - 'spec/services/google_cloud/generate_pipeline_service_spec.rb' - 'spec/services/gpg_keys/create_service_spec.rb' - - 'spec/services/groups/create_service_spec.rb' - 'spec/services/groups/deploy_tokens/revoke_service_spec.rb' - 'spec/services/groups/destroy_service_spec.rb' - 'spec/services/groups/group_links/create_service_spec.rb' diff --git a/.rubocop_todo/rspec/instance_variable.yml b/.rubocop_todo/rspec/instance_variable.yml index 51290ec9294..aca1a6d2d97 100644 --- a/.rubocop_todo/rspec/instance_variable.yml +++ b/.rubocop_todo/rspec/instance_variable.yml @@ -24,7 +24,6 @@ RSpec/InstanceVariable: - 'ee/spec/services/ee/notification_service_spec.rb' - 'ee/spec/services/ee/users/create_service_spec.rb' - 'ee/spec/services/geo/metrics_update_service_spec.rb' - - 'ee/spec/services/groups/create_service_spec.rb' - 'ee/spec/services/groups/participants_service_spec.rb' - 'ee/spec/services/projects/create_from_template_service_spec.rb' - 'ee/spec/services/projects/create_service_spec.rb' diff --git a/.rubocop_todo/rspec/named_subject.yml b/.rubocop_todo/rspec/named_subject.yml index eda8e34b6f4..79afdfa04f6 100644 --- a/.rubocop_todo/rspec/named_subject.yml +++ b/.rubocop_todo/rspec/named_subject.yml @@ -3278,7 +3278,6 @@ RSpec/NamedSubject: - 'spec/services/gpg_keys/create_service_spec.rb' - 'spec/services/gpg_keys/destroy_service_spec.rb' - 'spec/services/groups/autocomplete_service_spec.rb' - - 'spec/services/groups/create_service_spec.rb' - 'spec/services/groups/deploy_tokens/revoke_service_spec.rb' - 'spec/services/groups/group_links/create_service_spec.rb' - 'spec/services/groups/group_links/destroy_service_spec.rb' diff --git a/.rubocop_todo/rspec/return_from_stub.yml b/.rubocop_todo/rspec/return_from_stub.yml index 897d15c72ed..5aaa1847409 100644 --- a/.rubocop_todo/rspec/return_from_stub.yml +++ b/.rubocop_todo/rspec/return_from_stub.yml @@ -198,7 +198,6 @@ RSpec/ReturnFromStub: - 'spec/services/git/branch_hooks_service_spec.rb' - 'spec/services/git/branch_push_service_spec.rb' - 'spec/services/git/process_ref_changes_service_spec.rb' - - 'spec/services/groups/create_service_spec.rb' - 'spec/services/groups/nested_create_service_spec.rb' - 'spec/services/merge_requests/merge_orchestration_service_spec.rb' - 'spec/services/merge_requests/merge_service_spec.rb' diff --git a/GITLAB_KAS_VERSION b/GITLAB_KAS_VERSION index 985bd590db9..34326d62e6f 100644 --- a/GITLAB_KAS_VERSION +++ b/GITLAB_KAS_VERSION @@ -1 +1 @@ -v16.8.0-rc3 +v16.8.0 @@ -516,7 +516,7 @@ group :test do # Moved in `test` because https://gitlab.com/gitlab-org/gitlab/-/issues/217527 gem 'derailed_benchmarks', require: false # rubocop:todo Gemfile/MissingFeatureCategory - gem 'gitlab_quality-test_tooling', '~> 1.9.0', require: false, feature_category: :tooling + gem 'gitlab_quality-test_tooling', '~> 1.11.0', require: false, feature_category: :tooling end gem 'octokit', '~> 6.0' # rubocop:todo Gemfile/MissingFeatureCategory diff --git a/Gemfile.checksum b/Gemfile.checksum index cd6c98cf406..9048638cb6c 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -221,7 +221,7 @@ {"name":"gitlab-styles","version":"11.0.0","platform":"ruby","checksum":"0dd8ec066ce9955ac51d3616c6bfded30f75bb526f39ff392ece6f43d5b9406b"}, {"name":"gitlab_chronic_duration","version":"0.12.0","platform":"ruby","checksum":"0d766944d415b5c831f176871ee8625783fc0c5bfbef2d79a3a616f207ffc16d"}, {"name":"gitlab_omniauth-ldap","version":"2.2.0","platform":"ruby","checksum":"bb4d20acb3b123ed654a8f6a47d3fac673ece7ed0b6992edb92dca14bad2838c"}, -{"name":"gitlab_quality-test_tooling","version":"1.9.0","platform":"ruby","checksum":"0d47a72282e8e4d3dfc3094fe8eb2309a37c583570739a04e26afe38cd2da3a2"}, +{"name":"gitlab_quality-test_tooling","version":"1.11.0","platform":"ruby","checksum":"2146dbaddc5ffcaf4c6e876e6e9d9eb83bae25f378e0c9e5fca8d996ace3d02a"}, {"name":"globalid","version":"1.1.0","platform":"ruby","checksum":"b337e1746f0c8cb0a6c918234b03a1ddeb4966206ce288fbb57779f59b2d154f"}, {"name":"gon","version":"6.4.0","platform":"ruby","checksum":"e3a618d659392890f1aa7db420f17c75fd7d35aeb5f8fe003697d02c4b88d2f0"}, {"name":"google-apis-androidpublisher_v3","version":"0.34.0","platform":"ruby","checksum":"d7e1d7dd92f79c498fe2082222a1740d788e022e660c135564b3fd299cab5425"}, diff --git a/Gemfile.lock b/Gemfile.lock index 1c4388e3059..5386d595d52 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -728,7 +728,7 @@ GEM omniauth (>= 1.3, < 3) pyu-ruby-sasl (>= 0.0.3.3, < 0.1) rubyntlm (~> 0.5) - gitlab_quality-test_tooling (1.9.0) + gitlab_quality-test_tooling (1.11.0) activesupport (>= 6.1, < 7.2) amatch (~> 0.4.1) gitlab (~> 4.19) @@ -1909,7 +1909,7 @@ DEPENDENCIES gitlab-utils! gitlab_chronic_duration (~> 0.12) gitlab_omniauth-ldap (~> 2.2.0) - gitlab_quality-test_tooling (~> 1.9.0) + gitlab_quality-test_tooling (~> 1.11.0) gon (~> 6.4.0) google-apis-androidpublisher_v3 (~> 0.34.0) google-apis-cloudbilling_v1 (~> 0.21.0) diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js index c76e44a196d..b4933376d4e 100644 --- a/app/assets/javascripts/logo.js +++ b/app/assets/javascripts/logo.js @@ -13,7 +13,7 @@ export function initPortraitLogoDetection() { const isPortrait = img.height > img.width; if (isPortrait) { // Limit the width when the logo has portrait format - img.classList.replace('gl-h-9', 'gl-w-10'); + img.classList.replace('gl-h-10', 'gl-w-10'); } img.classList.remove('gl-visibility-hidden'); }, diff --git a/app/assets/javascripts/merge_requests/components/compare_app.vue b/app/assets/javascripts/merge_requests/components/compare_app.vue index 538aa090aa8..a753641ffd2 100644 --- a/app/assets/javascripts/merge_requests/components/compare_app.vue +++ b/app/assets/javascripts/merge_requests/components/compare_app.vue @@ -42,6 +42,11 @@ export default { required: false, default: () => ({}), }, + disabled: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -120,6 +125,7 @@ export default { :default="currentBranch" :toggle-class="toggleClass.branch" :data-qa-compare-side="compareSide" + :disabled="disabled" data-testid="compare-dropdown" @selected="selectBranch" /> diff --git a/app/assets/javascripts/merge_requests/components/compare_dropdown.vue b/app/assets/javascripts/merge_requests/components/compare_dropdown.vue index 20989206a51..35fbf4bc4e6 100644 --- a/app/assets/javascripts/merge_requests/components/compare_dropdown.vue +++ b/app/assets/javascripts/merge_requests/components/compare_dropdown.vue @@ -46,6 +46,11 @@ export default { required: false, default: '', }, + disabled: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -131,6 +136,7 @@ export default { :toggle-text="current.text || dropdownHeader" :header-text="dropdownHeader" :searching="isLoading" + :disabled="disabled" searchable class="gl-w-full dropdown-target-project" :toggle-class="[ diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js index 8cb1462c883..3d877bb3abb 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js @@ -16,8 +16,9 @@ if (mrNewCompareNode) { const sourceCompareEl = document.getElementById('js-source-project-dropdown'); const compareEl = document.querySelector('.js-merge-request-new-compare'); const targetBranch = Vue.observable({ name: '' }); - const currentSourceBranch = JSON.parse(sourceCompareEl.dataset.currentBranch); + const sourceBranch = Vue.observable(currentSourceBranch); + // eslint-disable-next-line no-new new Vue({ el: sourceCompareEl, @@ -52,6 +53,9 @@ if (mrNewCompareNode) { if (targetBranchName) { targetBranch.name = targetBranchName; } + + sourceBranch.value = branchName; + sourceBranch.text = branchName; }, }, render(h) { @@ -102,9 +106,14 @@ if (mrNewCompareNode) { return currentTargetBranch; }, + isDisabled() { + return !sourceBranch.value; + }, }, render(h) { - return h(CompareApp, { props: { currentBranch: this.currentBranch } }); + return h(CompareApp, { + props: { currentBranch: this.currentBranch, disabled: this.isDisabled }, + }); }, }); } else { diff --git a/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue b/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue index f71e6e6b405..79f0fdca061 100644 --- a/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue +++ b/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue @@ -17,14 +17,16 @@ import { import WorkItemDueDate from './work_item_due_date.vue'; import WorkItemAssignees from './work_item_assignees.vue'; import WorkItemLabels from './work_item_labels.vue'; -import WorkItemMilestone from './work_item_milestone.vue'; +import WorkItemMilestoneInline from './work_item_milestone_inline.vue'; +import WorkItemMilestoneWithEdit from './work_item_milestone_with_edit.vue'; import WorkItemParentInline from './work_item_parent_inline.vue'; import WorkItemParent from './work_item_parent_with_edit.vue'; export default { components: { WorkItemLabels, - WorkItemMilestone, + WorkItemMilestoneInline, + WorkItemMilestoneWithEdit, WorkItemAssignees, WorkItemDueDate, WorkItemParent, @@ -140,15 +142,28 @@ export default { :work-item-type="workItemType" @error="$emit('error', $event)" /> - <work-item-milestone - v-if="workItemMilestone" - :full-path="fullPath" - :work-item-id="workItem.id" - :work-item-milestone="workItemMilestone.milestone" - :work-item-type="workItemType" - :can-update="canUpdate" - @error="$emit('error', $event)" - /> + <template v-if="workItemMilestone"> + <work-item-milestone-with-edit + v-if="glFeatures.workItemsMvc2" + class="gl-mb-5" + :full-path="fullPath" + :work-item-id="workItem.id" + :work-item-milestone="workItemMilestone.milestone" + :work-item-type="workItemType" + :can-update="canUpdate" + @error="$emit('error', $event)" + /> + <work-item-milestone-inline + v-else + class="gl-mb-5" + :full-path="fullPath" + :work-item-id="workItem.id" + :work-item-milestone="workItemMilestone.milestone" + :work-item-type="workItemType" + :can-update="canUpdate" + @error="$emit('error', $event)" + /> + </template> <template v-if="workItemWeight"> <work-item-weight v-if="glFeatures.workItemsMvc2" diff --git a/app/assets/javascripts/work_items/components/work_item_milestone.vue b/app/assets/javascripts/work_items/components/work_item_milestone_inline.vue index dbeb3d4d3ff..dbeb3d4d3ff 100644 --- a/app/assets/javascripts/work_items/components/work_item_milestone.vue +++ b/app/assets/javascripts/work_items/components/work_item_milestone_inline.vue diff --git a/app/assets/javascripts/work_items/components/work_item_milestone_with_edit.vue b/app/assets/javascripts/work_items/components/work_item_milestone_with_edit.vue new file mode 100644 index 00000000000..9588d21a3c5 --- /dev/null +++ b/app/assets/javascripts/work_items/components/work_item_milestone_with_edit.vue @@ -0,0 +1,203 @@ +<script> +import { GlLink } from '@gitlab/ui'; +import * as Sentry from '~/sentry/sentry_browser_wrapper'; +import Tracking from '~/tracking'; +import { s__, __ } from '~/locale'; +import { MILESTONE_STATE } from '~/sidebar/constants'; +import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; +import WorkItemSidebarDropdownWidgetWithEdit from '~/work_items/components/shared/work_item_sidebar_dropdown_widget_with_edit.vue'; +import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql'; +import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; +import { + I18N_WORK_ITEM_ERROR_UPDATING, + sprintfWorkItem, + TRACKING_CATEGORY_SHOW, +} from '../constants'; + +export default { + i18n: { + milestone: s__('WorkItem|Milestone'), + none: s__('WorkItem|None'), + noMilestone: s__('WorkItem|No milestone'), + milestoneFetchError: s__( + 'WorkItem|Something went wrong while fetching milestones. Please try again.', + ), + expiredText: __('(expired)'), + }, + components: { + WorkItemSidebarDropdownWidgetWithEdit, + GlLink, + }, + mixins: [Tracking.mixin()], + props: { + fullPath: { + type: String, + required: true, + }, + workItemId: { + type: String, + required: true, + }, + workItemMilestone: { + type: Object, + required: false, + default: () => ({}), + }, + workItemType: { + type: String, + required: false, + default: '', + }, + canUpdate: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + searchTerm: '', + shouldFetch: false, + updateInProgress: false, + milestones: [], + localMilestone: this.workItemMilestone, + }; + }, + computed: { + tracking() { + return { + category: TRACKING_CATEGORY_SHOW, + label: 'item_milestone', + property: `type_${this.workItemType}`, + }; + }, + emptyPlaceholder() { + return this.canUpdate ? this.$options.i18n.noMilestone : this.$options.i18n.none; + }, + expired() { + return this.localMilestone?.expired ? ` ${this.$options.i18n.expiredText}` : ''; + }, + dropdownText() { + return this.localMilestone?.title + ? `${this.localMilestone?.title}${this.expired}` + : this.emptyPlaceholder; + }, + isLoadingMilestones() { + return this.$apollo.queries.milestones.loading; + }, + milestonesList() { + return this.milestones.map(({ id, title, expired }) => ({ + value: id, + text: title, + expired, + })); + }, + }, + apollo: { + milestones: { + query: projectMilestonesQuery, + debounce: DEFAULT_DEBOUNCE_AND_THROTTLE_MS, + variables() { + return { + fullPath: this.fullPath, + title: this.searchTerm, + state: MILESTONE_STATE.ACTIVE, + first: 20, + }; + }, + skip() { + return !this.shouldFetch; + }, + update(data) { + return data?.workspace?.attributes?.nodes || []; + }, + error() { + this.$emit('error', this.i18n.milestoneFetchError); + }, + }, + }, + methods: { + onDropdownShown() { + this.searchTerm = ''; + this.shouldFetch = true; + }, + search(searchTerm) { + this.searchTerm = searchTerm; + this.shouldFetch = true; + }, + itemExpiredText(item) { + return item.expired ? this.$options.i18n.expiredText : ''; + }, + updateMilestone(selectedMilestoneId) { + if (this.localMilestone?.id === selectedMilestoneId) { + return; + } + + this.localMilestone = selectedMilestoneId + ? this.milestones.find(({ id }) => id === selectedMilestoneId) + : null; + + this.track('updated_milestone'); + this.updateInProgress = true; + + this.$apollo + .mutate({ + mutation: updateWorkItemMutation, + variables: { + input: { + id: this.workItemId, + milestoneWidget: { + milestoneId: selectedMilestoneId, + }, + }, + }, + }) + .then(({ data }) => { + if (data.workItemUpdate.errors.length) { + throw new Error(data.workItemUpdate.errors.join('\n')); + } + }) + .catch((error) => { + this.localMilestone = this.workItemMilestone; + const msg = sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType); + this.$emit('error', msg); + Sentry.captureException(error); + }) + .finally(() => { + this.updateInProgress = false; + this.searchTerm = ''; + this.shouldFetch = false; + }); + }, + }, +}; +</script> + +<template> + <work-item-sidebar-dropdown-widget-with-edit + :dropdown-label="$options.i18n.milestone" + :can-update="canUpdate" + dropdown-name="milestone" + :loading="isLoadingMilestones" + :list-items="milestonesList" + :item-value="localMilestone" + :update-in-progress="updateInProgress" + :toggle-dropdown-text="dropdownText" + :header-text="__('Select milestone')" + :reset-button-label="__('Clear')" + data-testid="work-item-milestone-with-edit" + @dropdownShown="onDropdownShown" + @searchStarted="search" + @updateValue="updateMilestone" + > + <template #list-item="{ item }"> + <div>{{ item.text }}{{ itemExpiredText(item) }}</div> + <div v-if="item.title">{{ item.title }}</div> + </template> + <template #readonly> + <gl-link class="gl-text-gray-900!" :href="localMilestone.webPath"> + {{ localMilestone.title }}{{ expired }} + </gl-link> + </template> + </work-item-sidebar-dropdown-widget-with-edit> +</template> diff --git a/app/assets/stylesheets/page_bundles/login.scss b/app/assets/stylesheets/page_bundles/login.scss index 4cc44b01e60..bdbb2fb786f 100644 --- a/app/assets/stylesheets/page_bundles/login.scss +++ b/app/assets/stylesheets/page_bundles/login.scss @@ -16,43 +16,8 @@ top: 8px; } - .brand-holder { - font-size: 18px; - line-height: 1.5; - - p { - font-size: 16px; - color: $login-brand-holder-color; - } - - h3 { - font-size: 22px; - } - - img { - max-width: 100%; - margin-bottom: 30px; - } - - a { - font-weight: $gl-font-weight-bold; - } - } - - p { - font-size: 13px; - } - - .signin-text { - p { - margin-bottom: 0; - line-height: 1.5; - } - } - .borderless { - .login-box, - .omniauth-container { + .login-box { box-shadow: none; } } @@ -64,67 +29,6 @@ } } - .login-box, - .omniauth-container { - box-shadow: 0 0 0 1px $border-color; - border-radius: $border-radius; - - .login-heading h3 { - font-weight: $gl-font-weight-normal; - line-height: 1.5; - margin: 0 0 10px; - } - - .login-footer { - margin-top: 10px; - - p:last-child { - margin-bottom: 0; - } - } - - a.forgot { - float: right; - padding-top: 6px; - } - - .nav .active a { - background: transparent; - } - - // Styles the glowing border of focused input for username async validation - .login-body { - font-size: 13px; - - .username .validation-success { - color: $green-600; - } - - .username .validation-error { - color: $red-500; - } - - .terms .gl-form-checkbox { - @include gl-reset-font-size; - } - } - } - - .omniauth-container { - border-radius: $border-radius; - font-size: 13px; - - p { - margin: 0; - } - - form { - padding: 0; - border: 0; - background: none; - } - } - .new-session-tabs { &.nav-links-unboxed { border-color: transparent; diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb index 07a5e711d1c..81aa4757862 100644 --- a/app/helpers/appearances_helper.rb +++ b/app/helpers/appearances_helper.rb @@ -44,7 +44,7 @@ module AppearancesHelper end def brand_image - image_tag(brand_image_path, alt: brand_title, class: 'gl-visibility-hidden gl-h-9 js-portrait-logo-detection') + image_tag(brand_image_path, alt: brand_title, class: 'gl-visibility-hidden gl-h-10 js-portrait-logo-detection') end def brand_image_path diff --git a/app/helpers/count_helper.rb b/app/helpers/count_helper.rb index 62bb2e4da23..aed730850fd 100644 --- a/app/helpers/count_helper.rb +++ b/app/helpers/count_helper.rb @@ -11,7 +11,7 @@ module CountHelper # This will approximate the fork count by checking all counting all fork network # memberships, and deducting 1 for each root of the fork network. - # This might be inacurate as the root of the fork network might have been deleted. + # This might be inaccurate as the root of the fork network might have been deleted. # # This makes querying this information a lot more efficient and it should be # accurate enough for the instance wide statistics diff --git a/db/post_migrate/20240104223119_add_index_owasp_top_10_with_project_id_on_vulnerability_reads.rb b/db/post_migrate/20240104223119_add_index_owasp_top_10_with_project_id_on_vulnerability_reads.rb new file mode 100644 index 00000000000..99a2dccc8f5 --- /dev/null +++ b/db/post_migrate/20240104223119_add_index_owasp_top_10_with_project_id_on_vulnerability_reads.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddIndexOwaspTop10WithProjectIdOnVulnerabilityReads < Gitlab::Database::Migration[2.2] + disable_ddl_transaction! + + milestone '16.8' + + INDEX_NAME = 'index_vuln_reads_on_project_id_owasp_top_10' + + def up + add_concurrent_index :vulnerability_reads, [:project_id, :owasp_top_10], name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name :vulnerability_reads, INDEX_NAME + end +end diff --git a/db/post_migrate/20240108072319_add_fk_to_ci_build_trace_metadata_on_partition_id_and_trace_artifact_id2.rb b/db/post_migrate/20240108072319_add_fk_to_ci_build_trace_metadata_on_partition_id_and_trace_artifact_id2.rb new file mode 100644 index 00000000000..23219a9d90a --- /dev/null +++ b/db/post_migrate/20240108072319_add_fk_to_ci_build_trace_metadata_on_partition_id_and_trace_artifact_id2.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +class AddFkToCiBuildTraceMetadataOnPartitionIdAndTraceArtifactId2 < Gitlab::Database::Migration[2.2] + milestone '16.8' + disable_ddl_transaction! + + SOURCE_TABLE_NAME = :ci_build_trace_metadata + TARGET_TABLE_NAME = :ci_job_artifacts + COLUMN = :trace_artifact_id + TARGET_COLUMN = :id + FK_NAME = :fk_21d25cac1a_p + PARTITION_COLUMN = :partition_id + + def up + add_concurrent_foreign_key( + SOURCE_TABLE_NAME, + TARGET_TABLE_NAME, + column: [PARTITION_COLUMN, COLUMN], + target_column: [PARTITION_COLUMN, TARGET_COLUMN], + validate: true, + reverse_lock_order: true, + on_update: :cascade, + on_delete: :cascade, + name: FK_NAME + ) + end + + def down + with_lock_retries do + remove_foreign_key_if_exists( + SOURCE_TABLE_NAME, + TARGET_TABLE_NAME, + name: FK_NAME, + reverse_lock_order: true + ) + end + end +end diff --git a/db/post_migrate/20240108072546_add_fk_to_ci_job_artifact_states_on_partition_id_and_job_artifact_id2.rb b/db/post_migrate/20240108072546_add_fk_to_ci_job_artifact_states_on_partition_id_and_job_artifact_id2.rb new file mode 100644 index 00000000000..9b74f7019b9 --- /dev/null +++ b/db/post_migrate/20240108072546_add_fk_to_ci_job_artifact_states_on_partition_id_and_job_artifact_id2.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +class AddFkToCiJobArtifactStatesOnPartitionIdAndJobArtifactId2 < Gitlab::Database::Migration[2.2] + milestone '16.8' + disable_ddl_transaction! + + SOURCE_TABLE_NAME = :ci_job_artifact_states + TARGET_TABLE_NAME = :ci_job_artifacts + COLUMN = :job_artifact_id + TARGET_COLUMN = :id + FK_NAME = :fk_rails_80a9cba3b2_p + PARTITION_COLUMN = :partition_id + + def up + add_concurrent_foreign_key( + SOURCE_TABLE_NAME, + TARGET_TABLE_NAME, + column: [PARTITION_COLUMN, COLUMN], + target_column: [PARTITION_COLUMN, TARGET_COLUMN], + validate: true, + reverse_lock_order: true, + on_update: :cascade, + on_delete: :cascade, + name: FK_NAME + ) + end + + def down + with_lock_retries do + remove_foreign_key_if_exists( + SOURCE_TABLE_NAME, + TARGET_TABLE_NAME, + name: FK_NAME, + reverse_lock_order: true + ) + end + end +end diff --git a/db/schema_migrations/20240104223119 b/db/schema_migrations/20240104223119 new file mode 100644 index 00000000000..af271c3ce80 --- /dev/null +++ b/db/schema_migrations/20240104223119 @@ -0,0 +1 @@ +835c483008b589033f825535c381b963d5c20b2aa00f849376e05b69864f68ff
\ No newline at end of file diff --git a/db/schema_migrations/20240108072319 b/db/schema_migrations/20240108072319 new file mode 100644 index 00000000000..8f82a6baf77 --- /dev/null +++ b/db/schema_migrations/20240108072319 @@ -0,0 +1 @@ +67af5f06d58f67d7ce0f27c8fd5eb0856772b98443a59cd077b076494fed6634
\ No newline at end of file diff --git a/db/schema_migrations/20240108072546 b/db/schema_migrations/20240108072546 new file mode 100644 index 00000000000..997e37dba92 --- /dev/null +++ b/db/schema_migrations/20240108072546 @@ -0,0 +1 @@ +981e263b0c9715324d86ed29534465ebcf2d37c8bb9e5dc0d93b9abf11f264d4
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index c444798f901..82417b9b6be 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -35671,6 +35671,8 @@ CREATE INDEX index_vuln_reads_on_casted_cluster_agent_id_where_it_is_null ON vul CREATE INDEX index_vuln_reads_on_namespace_id_state_severity_and_vuln_id ON vulnerability_reads USING btree (namespace_id, state, severity, vulnerability_id DESC); +CREATE INDEX index_vuln_reads_on_project_id_owasp_top_10 ON vulnerability_reads USING btree (project_id, owasp_top_10); + CREATE INDEX index_vuln_reads_on_project_id_state_severity_and_vuln_id ON vulnerability_reads USING btree (project_id, state, severity, vulnerability_id DESC); CREATE INDEX index_vulnerabilities_common_finder_query_on_default_branch ON vulnerabilities USING btree (project_id, state, report_type, present_on_default_branch, severity, id); @@ -38121,7 +38123,7 @@ ALTER TABLE ONLY ci_build_trace_metadata ADD CONSTRAINT fk_21d25cac1a FOREIGN KEY (trace_artifact_id) REFERENCES ci_job_artifacts(id) ON DELETE CASCADE; ALTER TABLE ONLY ci_build_trace_metadata - ADD CONSTRAINT fk_21d25cac1a_p FOREIGN KEY (partition_id, trace_artifact_id) REFERENCES ci_job_artifacts(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE NOT VALID; + ADD CONSTRAINT fk_21d25cac1a_p FOREIGN KEY (partition_id, trace_artifact_id) REFERENCES ci_job_artifacts(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE; ALTER TABLE ONLY users_star_projects ADD CONSTRAINT fk_22cd27ddfc FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; @@ -40062,7 +40064,7 @@ ALTER TABLE ONLY ci_job_artifact_states ADD CONSTRAINT fk_rails_80a9cba3b2 FOREIGN KEY (job_artifact_id) REFERENCES ci_job_artifacts(id) ON DELETE CASCADE; ALTER TABLE ONLY ci_job_artifact_states - ADD CONSTRAINT fk_rails_80a9cba3b2_p FOREIGN KEY (partition_id, job_artifact_id) REFERENCES ci_job_artifacts(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE NOT VALID; + ADD CONSTRAINT fk_rails_80a9cba3b2_p FOREIGN KEY (partition_id, job_artifact_id) REFERENCES ci_job_artifacts(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE; ALTER TABLE ONLY approval_merge_request_rules_users ADD CONSTRAINT fk_rails_80e6801803 FOREIGN KEY (approval_merge_request_rule_id) REFERENCES approval_merge_request_rules(id) ON DELETE CASCADE; diff --git a/doc/architecture/blueprints/cells/proposal-stateless-router-with-buffering-requests.md b/doc/architecture/blueprints/cells/proposal-stateless-router-with-buffering-requests.md index fa149e4a3dc..699a41879a9 100644 --- a/doc/architecture/blueprints/cells/proposal-stateless-router-with-buffering-requests.md +++ b/doc/architecture/blueprints/cells/proposal-stateless-router-with-buffering-requests.md @@ -2,8 +2,11 @@ stage: enablement group: Tenant Scale description: 'Cells Stateless Router Proposal' +status: rejected --- +_This proposal was superseded by the [routing service proposal](routing-service.md)_ + <!-- vale gitlab.FutureTense = NO --> This document is a work-in-progress and represents a very early state of the diff --git a/doc/architecture/blueprints/cells/proposal-stateless-router-with-routes-learning.md b/doc/architecture/blueprints/cells/proposal-stateless-router-with-routes-learning.md index 2978936bf4e..72b96e9ab8c 100644 --- a/doc/architecture/blueprints/cells/proposal-stateless-router-with-routes-learning.md +++ b/doc/architecture/blueprints/cells/proposal-stateless-router-with-routes-learning.md @@ -2,8 +2,11 @@ stage: enablement group: Tenant Scale description: 'Cells Stateless Router Proposal' +status: rejected --- +_This proposal was superseded by the [routing service proposal](routing-service.md)_ + <!-- vale gitlab.FutureTense = NO --> This document is a work-in-progress and represents a very early state of the diff --git a/doc/development/search/advanced_search_migration_styleguide.md b/doc/development/search/advanced_search_migration_styleguide.md index 87083d1a36f..b79ab6561d5 100644 --- a/doc/development/search/advanced_search_migration_styleguide.md +++ b/doc/development/search/advanced_search_migration_styleguide.md @@ -225,6 +225,53 @@ class MigrationName < Elastic::Migration end ``` +#### `Search::Elastic::MigrationDeleteBasedOnSchemaVersion` + +Deletes all documents in the index that stores the specified document type and has `schema_version` less than the given value. + +Requires the `DOCUMENT_TYPE` constant and `schema_version` method. +The index mapping must have a `schema_version` integer field in a `YYMM` format. + +```ruby +class MigrationName < Elastic::Migration + include ::Search::Elastic::MigrationDeleteBasedOnSchemaVersion + + DOCUMENT_TYPE = Issue + + batch_size 10_000 + batched! + throttle_delay 1.minute + retry_on_failure + + def schema_version + 23_12 + end +end +``` + +#### `Search::Elastic::MigrationDatabaseBackfillHelper` + +Reindexes all documents in the database to the elastic search index respecting the `limited_indexing` setting. + +Requires the `DOCUMENT_TYPE` constant and `respect_limited_indexing?` method. + +```ruby +class MigrationName < Elastic::Migration + include ::Search::Elastic::MigrationDatabaseBackfillHelper + + batch_size 10_000 + batched! + throttle_delay 1.minute + retry_on_failure + + DOCUMENT_TYPE = Issue + + def respect_limited_indexing? + true + end +end +``` + #### `Elastic::MigrationHelper` Contains methods you can use when a migration doesn't fit the previous examples. diff --git a/doc/user/application_security/security_dashboard/index.md b/doc/user/application_security/security_dashboard/index.md index 405017ab023..e9f3a3a2c0b 100644 --- a/doc/user/application_security/security_dashboard/index.md +++ b/doc/user/application_security/security_dashboard/index.md @@ -20,6 +20,12 @@ The data provided by the Security Dashboards can be used supply to insight on wh <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> For an overview, see [Security Dashboard](https://www.youtube.com/watch?v=Uo-pDns1OpQ). +## Vulnerability metrics in the Value Streams Dashboard + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/383697) in GitLab 16.0. + +You can view vulnerability metrics also in the [Value Streams Dashboard](../../../user/analytics/value_streams_dashboard.md) comparison panel, which helps you understand security exposure in the context of your organization's software delivery workflows. + ## Prerequisites To view the Security Dashboards, the following is required: diff --git a/doc/user/project/remote_development/connect_machine.md b/doc/user/project/remote_development/connect_machine.md index df2921b184c..b37a2c5fc0f 100644 --- a/doc/user/project/remote_development/connect_machine.md +++ b/doc/user/project/remote_development/connect_machine.md @@ -13,9 +13,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w FLAG: On self-managed GitLab, by default this feature is available. To hide the feature, an administrator can [disable the feature flag](../../../administration/feature_flags.md) named `vscode_web_ide`. On GitLab.com, this feature is available. The feature is not ready for production use. -WARNING: -This feature is in [Beta](../../../policy/experiment-beta-support.md#beta) and subject to change without notice. - This tutorial shows you how to: - Create a development environment outside of GitLab. diff --git a/doc/user/project/remote_development/index.md b/doc/user/project/remote_development/index.md index c11d8591745..65445e54949 100644 --- a/doc/user/project/remote_development/index.md +++ b/doc/user/project/remote_development/index.md @@ -13,9 +13,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w FLAG: On self-managed GitLab, by default this feature is available. To hide the feature, an administrator can [disable the feature flag](../../../administration/feature_flags.md) named `vscode_web_ide`. On GitLab.com, this feature is available. The feature is not ready for production use. -WARNING: -This feature is in [Beta](../../../policy/experiment-beta-support.md#beta) and subject to change without notice. - You can use remote development to write and compile code hosted on GitLab. With remote development, you can: diff --git a/doc/user/search/advanced_search.md b/doc/user/search/advanced_search.md index 3b715fb13da..757231ffb80 100644 --- a/doc/user/search/advanced_search.md +++ b/doc/user/search/advanced_search.md @@ -30,10 +30,10 @@ You can use advanced search in: ## Enable advanced search -- On GitLab.com, advanced search is enabled for groups with paid subscriptions. -- For self-managed GitLab instances, an administrator must +- For [GitLab SaaS](../../subscriptions/gitlab_com/index.md) and [GitLab Dedicated](../../subscriptions/gitlab_dedicated/index.md), + advanced search is enabled in paid subscriptions. +- For [GitLab self-managed](../../subscriptions/self_managed/index.md), an administrator must [enable advanced search](../../integration/advanced_search/elasticsearch.md#enable-advanced-search). -- For GitLab Dedicated, advanced search is enabled. ## Syntax diff --git a/doc/user/workspace/gitlab_agent_configuration.md b/doc/user/workspace/gitlab_agent_configuration.md index 0e35c72c5ef..bf48521b612 100644 --- a/doc/user/workspace/gitlab_agent_configuration.md +++ b/doc/user/workspace/gitlab_agent_configuration.md @@ -85,6 +85,7 @@ The default value is: ```yaml remote_development: network_policy: + enabled: true egress: - allow: "0.0.0.0/0" except: diff --git a/doc/user/workspace/index.md b/doc/user/workspace/index.md index 309c5d19a51..5fa6108de6f 100644 --- a/doc/user/workspace/index.md +++ b/doc/user/workspace/index.md @@ -67,32 +67,37 @@ The devfile is used to automatically configure the development environment with This way, you can create consistent and reproducible development environments regardless of the machine or platform you use. -### Relevant schema properties - -GitLab only supports the `container` and `volume` components in [devfile 2.2.0](https://devfile.io/docs/2.2.0/devfile-schema). -Use the `container` component to define a container image as the execution environment for a devfile workspace. +### Validation rules + +- `schemaVersion` must be [`2.2.0`](https://devfile.io/docs/2.2.0/devfile-schema). +- The devfile must have at least one component. +- For `components`: + - Names must not start with `gl-`. + - Only [`container`](#container-component-type) and `volume` are supported. +- For `commands`, IDs must not start with `gl-`. +- For `events`: + - Names must not start with `gl-`. + - Only `preStart` is supported. +- `parent`, `projects`, and `starterProjects` are not supported. +- For `variables`, keys must not start with `gl-`, `gl_`, `GL-`, or `GL_`. + +### `container` component type + +Use the `container` component type to define a container image as the execution environment for a workspace. You can specify the base image, dependencies, and other settings. -Only these properties are relevant to the GitLab implementation of the `container` component: - -| Properties | Definition | -|----------------| ----------------------------------------------------------------------------------| -| `image` | Name of the container image to use for the workspace. | -| `memoryRequest`| Minimum amount of memory the container can use. | -| `memoryLimit` | Maximum amount of memory the container can use. | -| `cpuRequest` | Minimum amount of CPU the container can use. | -| `cpuLimit` | Maximum amount of CPU the container can use. | -| `env` | Environment variables to use in the container. | -| `endpoints` | Port mappings to expose from the container. | -| `volumeMounts` | Storage volume to mount in the container. | - -### Using variables in a devfile - -You can define variables to use in your devfile. -The `variables` object is a map of name-value pairs that you can use for string replacement in the devfile. +The `container` component type supports the following schema properties only: -Variables cannot have names that start with `gl-`, `gl_`, `GL-`, or `GL_`. -For more information about how and where to use variables, see the [devfile documentation](https://devfile.io/docs/2.2.0/defining-variables). +| Property | Description | +|----------------| -------------------------------------------------------------------------------------------------------------------------------| +| `image` | Name of the container image to use for the workspace. | +| `memoryRequest`| Minimum amount of memory the container can use. | +| `memoryLimit` | Maximum amount of memory the container can use. | +| `cpuRequest` | Minimum amount of CPU the container can use. | +| `cpuLimit` | Maximum amount of CPU the container can use. | +| `env` | Environment variables to use in the container. Names must not start with `gl-`. | +| `endpoints` | Port mappings to expose from the container. Names must not start with `gl-`. | +| `volumeMounts` | Storage volume to mount in the container. | ### Example configurations @@ -181,4 +186,3 @@ For more information, see the ## Related topics - [GitLab workspaces demo](https://go.gitlab.com/qtu66q) -- [Developer documentation](../../development/remote_development/index.md)] diff --git a/spec/features/merge_request/user_creates_mr_spec.rb b/spec/features/merge_request/user_creates_mr_spec.rb index 950b64bb395..5f6e465d011 100644 --- a/spec/features/merge_request/user_creates_mr_spec.rb +++ b/spec/features/merge_request/user_creates_mr_spec.rb @@ -74,6 +74,9 @@ RSpec.describe 'Merge request > User creates MR', feature_category: :code_review visit project_new_merge_request_path(source_project) + find('.js-source-branch').click + select_listbox_item('master') + first('.js-target-project').click select_listbox_item(target_project.full_path) diff --git a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb index daa84227adc..a68bdfd7356 100644 --- a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb +++ b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb @@ -43,6 +43,9 @@ RSpec.describe 'Merge request > User selects branches for new MR', :js, feature_ expect(page).to have_content('Source branch') expect(page).to have_content('Target branch') + find('.js-source-branch').click + select_listbox_item('master') + first('.js-target-branch').click find('.gl-listbox-search-input').native.send_keys 'v1.1.0' select_listbox_item('v1.1.0') diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb index 87ed4ced684..c5ad7bca824 100644 --- a/spec/features/users/login_spec.rb +++ b/spec/features/users/login_spec.rb @@ -789,7 +789,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_ visit new_user_session_path image = find('img.js-portrait-logo-detection') - expect(image['class']).to include('gl-h-9') + expect(image['class']).to include('gl-h-10') end it 'renders link to sign up path' do diff --git a/spec/frontend/logo_spec.js b/spec/frontend/logo_spec.js index 8e39e75bd3b..51f47fb89ba 100644 --- a/spec/frontend/logo_spec.js +++ b/spec/frontend/logo_spec.js @@ -10,7 +10,7 @@ describe('initPortraitLogoDetection', () => { }; beforeEach(() => { - setHTMLFixture('<img class="gl-visibility-hidden gl-h-9 js-portrait-logo-detection" />'); + setHTMLFixture('<img class="gl-visibility-hidden gl-h-10 js-portrait-logo-detection" />'); initPortraitLogoDetection(); img = document.querySelector('img'); }); @@ -27,12 +27,12 @@ describe('initPortraitLogoDetection', () => { it('removes gl-visibility-hidden', () => { expect(img.classList).toContain('gl-visibility-hidden'); - expect(img.classList).toContain('gl-h-9'); + expect(img.classList).toContain('gl-h-10'); loadImage(); expect(img.classList).not.toContain('gl-visibility-hidden'); - expect(img.classList).toContain('gl-h-9'); + expect(img.classList).toContain('gl-h-10'); }); }); @@ -44,7 +44,7 @@ describe('initPortraitLogoDetection', () => { it('removes gl-visibility-hidden', () => { expect(img.classList).toContain('gl-visibility-hidden'); - expect(img.classList).toContain('gl-h-9'); + expect(img.classList).toContain('gl-h-10'); loadImage(); diff --git a/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js b/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js index 48ec84ceb85..43f7027406f 100644 --- a/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js +++ b/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js @@ -3,7 +3,8 @@ import { shallowMount } from '@vue/test-utils'; import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue'; import WorkItemDueDate from '~/work_items/components/work_item_due_date.vue'; import WorkItemLabels from '~/work_items/components/work_item_labels.vue'; -import WorkItemMilestone from '~/work_items/components/work_item_milestone.vue'; +import WorkItemMilestoneInline from '~/work_items/components/work_item_milestone_inline.vue'; +import WorkItemMilestoneWithEdit from '~/work_items/components/work_item_milestone_with_edit.vue'; import WorkItemParentInline from '~/work_items/components/work_item_parent_inline.vue'; import WorkItemParent from '~/work_items/components/work_item_parent_with_edit.vue'; import waitForPromises from 'helpers/wait_for_promises'; @@ -24,7 +25,8 @@ describe('WorkItemAttributesWrapper component', () => { const findWorkItemDueDate = () => wrapper.findComponent(WorkItemDueDate); const findWorkItemAssignees = () => wrapper.findComponent(WorkItemAssignees); const findWorkItemLabels = () => wrapper.findComponent(WorkItemLabels); - const findWorkItemMilestone = () => wrapper.findComponent(WorkItemMilestone); + const findWorkItemMilestone = () => wrapper.findComponent(WorkItemMilestoneWithEdit); + const findWorkItemMilestoneInline = () => wrapper.findComponent(WorkItemMilestoneInline); const findWorkItemParentInline = () => wrapper.findComponent(WorkItemParentInline); const findWorkItemParent = () => wrapper.findComponent(WorkItemParent); @@ -110,6 +112,26 @@ describe('WorkItemAttributesWrapper component', () => { expect(findWorkItemMilestone().exists()).toBe(exists); }); + + it.each` + description | milestoneWidgetInlinePresent | milestoneWidgetWithEditPresent | workItemsMvc2FlagEnabled + ${'renders WorkItemMilestone when workItemsMvc2 enabled'} | ${false} | ${true} | ${true} + ${'renders WorkItemMilestoneInline when workItemsMvc2 disabled'} | ${true} | ${false} | ${false} + `( + '$description', + async ({ + milestoneWidgetInlinePresent, + milestoneWidgetWithEditPresent, + workItemsMvc2FlagEnabled, + }) => { + createComponent({ workItemsMvc2: workItemsMvc2FlagEnabled }); + + await waitForPromises(); + + expect(findWorkItemMilestone().exists()).toBe(milestoneWidgetWithEditPresent); + expect(findWorkItemMilestoneInline().exists()).toBe(milestoneWidgetInlinePresent); + }, + ); }); describe('parent widget', () => { diff --git a/spec/frontend/work_items/components/work_item_milestone_spec.js b/spec/frontend/work_items/components/work_item_milestone_inline_spec.js index fc2c5eb2af2..75c5763914a 100644 --- a/spec/frontend/work_items/components/work_item_milestone_spec.js +++ b/spec/frontend/work_items/components/work_item_milestone_inline_spec.js @@ -2,7 +2,9 @@ import { GlCollapsibleListbox, GlListboxItem, GlSkeletonLoader, GlFormGroup } fr import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; -import WorkItemMilestone, { noMilestoneId } from '~/work_items/components/work_item_milestone.vue'; +import WorkItemMilestoneInline, { + noMilestoneId, +} from '~/work_items/components/work_item_milestone_inline.vue'; import createMockApollo from 'helpers/mock_apollo_helper'; import { mockTracking } from 'helpers/tracking_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; @@ -18,7 +20,7 @@ import { updateWorkItemMutationResponse, } from '../mock_data'; -describe('WorkItemMilestone component', () => { +describe('WorkItemMilestoneInline component', () => { Vue.use(VueApollo); let wrapper; @@ -51,7 +53,7 @@ describe('WorkItemMilestone component', () => { searchQueryHandler = successSearchQueryHandler, mutationHandler = successUpdateWorkItemMutationHandler, } = {}) => { - wrapper = shallowMountExtended(WorkItemMilestone, { + wrapper = shallowMountExtended(WorkItemMilestoneInline, { apolloProvider: createMockApollo([ [projectMilestonesQuery, searchQueryHandler], [updateWorkItemMutation, mutationHandler], @@ -73,13 +75,13 @@ describe('WorkItemMilestone component', () => { createComponent(); expect(findInputGroup().exists()).toBe(true); - expect(findInputGroup().attributes('label')).toBe(WorkItemMilestone.i18n.MILESTONE); + expect(findInputGroup().attributes('label')).toBe(WorkItemMilestoneInline.i18n.MILESTONE); }); describe('Default text with canUpdate false and milestone value', () => { describe.each` description | milestone | value - ${'when no milestone'} | ${null} | ${WorkItemMilestone.i18n.NONE} + ${'when no milestone'} | ${null} | ${WorkItemMilestoneInline.i18n.NONE} ${'when milestone set'} | ${mockMilestoneWidgetResponse} | ${mockMilestoneWidgetResponse.title} `('$description', ({ milestone, value }) => { it(`has a value of "${value}"`, () => { @@ -95,7 +97,9 @@ describe('WorkItemMilestone component', () => { it(`has a value of "Add to milestone"`, () => { createComponent({ canUpdate: true, milestone: null }); - expect(findDropdown().props('toggleText')).toBe(WorkItemMilestone.i18n.MILESTONE_PLACEHOLDER); + expect(findDropdown().props('toggleText')).toBe( + WorkItemMilestoneInline.i18n.MILESTONE_PLACEHOLDER, + ); }); }); @@ -111,7 +115,7 @@ describe('WorkItemMilestone component', () => { searchQueryHandler: successSearchWithNoMatchingMilestones, }); - expect(findNoResultsText().text()).toBe(WorkItemMilestone.i18n.NO_MATCHING_RESULTS); + expect(findNoResultsText().text()).toBe(WorkItemMilestoneInline.i18n.NO_MATCHING_RESULTS); expect(findDropdownItems()).toHaveLength(1); }); }); @@ -162,7 +166,7 @@ describe('WorkItemMilestone component', () => { await waitForPromises(); expect(findDropdown().props()).toMatchObject({ loading: false, - toggleText: WorkItemMilestone.i18n.MILESTONE_PLACEHOLDER, + toggleText: WorkItemMilestoneInline.i18n.MILESTONE_PLACEHOLDER, toggleClass: expect.arrayContaining(['gl-text-gray-500!']), }); }); diff --git a/spec/frontend/work_items/components/work_item_milestone_with_edit_spec.js b/spec/frontend/work_items/components/work_item_milestone_with_edit_spec.js new file mode 100644 index 00000000000..58a57978126 --- /dev/null +++ b/spec/frontend/work_items/components/work_item_milestone_with_edit_spec.js @@ -0,0 +1,209 @@ +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import WorkItemMilestone from '~/work_items/components/work_item_milestone_with_edit.vue'; +import WorkItemSidebarDropdownWidgetWithEdit from '~/work_items/components/shared/work_item_sidebar_dropdown_widget_with_edit.vue'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { mockTracking } from 'helpers/tracking_helper'; +import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; +import waitForPromises from 'helpers/wait_for_promises'; +import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants'; +import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; +import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql'; +import { + projectMilestonesResponse, + projectMilestonesResponseWithNoMilestones, + mockMilestoneWidgetResponse, + updateWorkItemMutationErrorResponse, + updateWorkItemMutationResponse, +} from '../mock_data'; + +describe('WorkItemMilestoneWithEdit component', () => { + Vue.use(VueApollo); + + let wrapper; + + const workItemId = 'gid://gitlab/WorkItem/1'; + const workItemType = 'Task'; + + const findSidebarDropdownWidget = () => + wrapper.findComponent(WorkItemSidebarDropdownWidgetWithEdit); + + const successSearchQueryHandler = jest.fn().mockResolvedValue(projectMilestonesResponse); + const successSearchWithNoMatchingMilestones = jest + .fn() + .mockResolvedValue(projectMilestonesResponseWithNoMilestones); + const successUpdateWorkItemMutationHandler = jest + .fn() + .mockResolvedValue(updateWorkItemMutationResponse); + + const showDropdown = () => findSidebarDropdownWidget().vm.$emit('dropdownShown'); + + const createComponent = ({ + mountFn = shallowMountExtended, + canUpdate = true, + milestone = mockMilestoneWidgetResponse, + searchQueryHandler = successSearchQueryHandler, + mutationHandler = successUpdateWorkItemMutationHandler, + } = {}) => { + wrapper = mountFn(WorkItemMilestone, { + apolloProvider: createMockApollo([ + [projectMilestonesQuery, searchQueryHandler], + [updateWorkItemMutation, mutationHandler], + ]), + propsData: { + fullPath: 'full-path', + canUpdate, + workItemMilestone: milestone, + workItemId, + workItemType, + }, + }); + }; + + it('has "Milestone" label', () => { + createComponent(); + + expect(findSidebarDropdownWidget().props('dropdownLabel')).toBe('Milestone'); + }); + + describe('Default text with canUpdate false and milestone value', () => { + describe.each` + description | milestone | value + ${'when no milestone'} | ${null} | ${'None'} + ${'when milestone set'} | ${mockMilestoneWidgetResponse} | ${mockMilestoneWidgetResponse.title} + `('$description', ({ milestone, value }) => { + it(`has a value of "${value}"`, () => { + createComponent({ mountFn: mountExtended, canUpdate: false, milestone }); + + expect(findSidebarDropdownWidget().props('canUpdate')).toBe(false); + expect(wrapper.text()).toContain(value); + }); + }); + }); + + describe('Dropdown search', () => { + it('shows no matching results when no items', () => { + createComponent({ + searchQueryHandler: successSearchWithNoMatchingMilestones, + }); + + expect(findSidebarDropdownWidget().props('listItems')).toHaveLength(0); + }); + }); + + describe('Dropdown options', () => { + beforeEach(() => { + createComponent({ canUpdate: true }); + }); + + it('calls successSearchQueryHandler with variables when dropdown is opened', async () => { + showDropdown(); + await nextTick(); + jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS); + + await waitForPromises(); + + expect(successSearchQueryHandler).toHaveBeenCalledWith({ + first: 20, + fullPath: 'full-path', + state: 'active', + title: '', + }); + }); + + it('shows the skeleton loader when the items are being fetched on click', async () => { + showDropdown(); + await nextTick(); + jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS); + + await nextTick(); + + expect(findSidebarDropdownWidget().props('loading')).toBe(true); + }); + + it('shows the milestones in dropdown when the items have finished fetching', async () => { + showDropdown(); + await nextTick(); + jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS); + + await waitForPromises(); + + expect(findSidebarDropdownWidget().props('loading')).toBe(false); + expect(findSidebarDropdownWidget().props('listItems')).toHaveLength( + projectMilestonesResponse.data.workspace.attributes.nodes.length, + ); + }); + + it('changes the milestone to null when clicked on no milestone', async () => { + showDropdown(); + await nextTick(); + jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS); + + findSidebarDropdownWidget().vm.$emit('updateValue', null); + + await nextTick(); + expect(findSidebarDropdownWidget().props('updateInProgress')).toBe(true); + + await waitForPromises(); + expect(findSidebarDropdownWidget().props('updateInProgress')).toBe(false); + expect(findSidebarDropdownWidget().props('itemValue')).toBe(null); + }); + + it('changes the milestone to the selected milestone', async () => { + const milestoneAtIndex = projectMilestonesResponse.data.workspace.attributes.nodes[0]; + + showDropdown(); + await nextTick(); + jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS); + + await waitForPromises(); + findSidebarDropdownWidget().vm.$emit('updateValue', milestoneAtIndex.id); + + await nextTick(); + + expect(findSidebarDropdownWidget().props('itemValue').title).toBe(milestoneAtIndex.title); + }); + }); + + describe('Error handlers', () => { + it.each` + errorType | expectedErrorMessage | mockValue | resolveFunction + ${'graphql error'} | ${'Something went wrong while updating the task. Please try again.'} | ${updateWorkItemMutationErrorResponse} | ${'mockResolvedValue'} + ${'network error'} | ${'Something went wrong while updating the task. Please try again.'} | ${new Error()} | ${'mockRejectedValue'} + `( + 'emits an error when there is a $errorType', + async ({ mockValue, expectedErrorMessage, resolveFunction }) => { + createComponent({ + mutationHandler: jest.fn()[resolveFunction](mockValue), + canUpdate: true, + }); + + showDropdown(); + findSidebarDropdownWidget().vm.$emit('updateValue', null); + + await waitForPromises(); + + expect(wrapper.emitted('error')).toEqual([[expectedErrorMessage]]); + }, + ); + }); + + describe('Tracking event', () => { + it('tracks updating the milestone', async () => { + const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + createComponent({ canUpdate: true }); + + showDropdown(); + findSidebarDropdownWidget().vm.$emit('updateValue', null); + + await waitForPromises(); + + expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'updated_milestone', { + category: TRACKING_CATEGORY_SHOW, + label: 'item_milestone', + property: 'type_Task', + }); + }); + }); +}); diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb index a6c2b593e40..8ce69d12b3f 100644 --- a/spec/services/groups/create_service_spec.rb +++ b/spec/services/groups/create_service_spec.rb @@ -3,45 +3,58 @@ require 'spec_helper' RSpec.describe Groups::CreateService, '#execute', feature_category: :groups_and_projects do - let!(:user) { create(:user) } - let!(:group_params) { { path: "group_path", visibility_level: Gitlab::VisibilityLevel::PUBLIC } } + let_it_be(:user, reload: true) { create(:user) } + let(:current_user) { user } + let(:group_params) { { path: 'group_path', visibility_level: Gitlab::VisibilityLevel::PUBLIC }.merge(extra_params) } + let(:extra_params) { {} } + let(:created_group) { response } - subject(:execute) { service.execute } + subject(:response) { described_class.new(current_user, group_params).execute } shared_examples 'has sync-ed traversal_ids' do - specify { expect(subject.reload.traversal_ids).to eq([subject.parent&.traversal_ids, subject.id].flatten.compact) } + specify do + expect(created_group.traversal_ids).to eq([created_group.parent&.traversal_ids, created_group.id].flatten.compact) + end + end + + shared_examples 'creating a group' do + specify do + expect { response }.to change { Group.count } + expect(created_group).to be_persisted + end end - describe 'visibility level restrictions' do - let!(:service) { described_class.new(user, group_params) } + shared_examples 'does not create a group' do + specify do + expect { response }.not_to change { Group.count } + expect(created_group).not_to be_persisted + end + end - context "create groups without restricted visibility level" do - it { is_expected.to be_persisted } + context 'for visibility level restrictions' do + context 'without restricted visibility level' do + it_behaves_like 'creating a group' end - context "cannot create group with restricted visibility level" do + context 'with restricted visibility level' do before do - allow_any_instance_of(ApplicationSetting).to receive(:restricted_visibility_levels).and_return([Gitlab::VisibilityLevel::PUBLIC]) + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) end - it { is_expected.not_to be_persisted } + it_behaves_like 'does not create a group' end end - context 'when `setup_for_company:true` is passed' do - let(:params) { group_params.merge(setup_for_company: true) } - let(:service) { described_class.new(user, params) } - let(:created_group) { service.execute } + context 'with `setup_for_company` attribute' do + let(:extra_params) { { setup_for_company: true } } - it 'creates group with the specified setup_for_company' do + it 'has the specified setup_for_company' do expect(created_group.setup_for_company).to eq(true) end end - context 'creating a group with `default_branch_protection` attribute' do - let(:params) { group_params.merge(default_branch_protection: Gitlab::Access::PROTECTION_NONE) } - let(:service) { described_class.new(user, params) } - let(:created_group) { service.execute } + context 'with `default_branch_protection` attribute' do + let(:extra_params) { { default_branch_protection: Gitlab::Access::PROTECTION_NONE } } context 'for users who have the ability to create a group with `default_branch_protection`' do it 'creates group with the specified branch protection level' do @@ -52,23 +65,22 @@ RSpec.describe Groups::CreateService, '#execute', feature_category: :groups_and_ context 'for users who do not have the ability to create a group with `default_branch_protection`' do it 'does not create the group with the specified branch protection level' do allow(Ability).to receive(:allowed?).and_call_original - allow(Ability).to receive(:allowed?).with(user, :create_group_with_default_branch_protection) { false } + allow(Ability).to receive(:allowed?).with(user, :create_group_with_default_branch_protection).and_return(false) expect(created_group.default_branch_protection).not_to eq(Gitlab::Access::PROTECTION_NONE) end end end - context 'creating a group with `default_branch_protection_defaults` attribute' do + context 'with `default_branch_protection_defaults` attribute' do let(:branch_protection) { ::Gitlab::Access::BranchProtection.protected_against_developer_pushes.stringify_keys } - let(:params) { group_params.merge(default_branch_protection_defaults: branch_protection) } - let(:service) { described_class.new(user, params) } - let(:created_group) { service.execute } + let(:extra_params) { { default_branch_protection_defaults: branch_protection } } context 'for users who have the ability to create a group with `default_branch_protection`' do before do allow(Ability).to receive(:allowed?).and_call_original - allow(Ability).to receive(:allowed?).with(user, :update_default_branch_protection, an_instance_of(Group)).and_return(true) + allow(Ability) + .to receive(:allowed?).with(user, :update_default_branch_protection, an_instance_of(Group)).and_return(true) end it 'creates group with the specified default branch protection settings' do @@ -79,31 +91,26 @@ RSpec.describe Groups::CreateService, '#execute', feature_category: :groups_and_ context 'for users who do not have the ability to create a group with `default_branch_protection_defaults`' do it 'does not create the group with the specified default branch protection settings' do allow(Ability).to receive(:allowed?).and_call_original - allow(Ability).to receive(:allowed?).with(user, :create_group_with_default_branch_protection) { false } + allow(Ability).to receive(:allowed?).with(user, :create_group_with_default_branch_protection).and_return(false) expect(created_group.default_branch_protection_defaults).not_to eq(Gitlab::Access::PROTECTION_NONE) end end end - context 'creating a group with `allow_mfa_for_subgroups` attribute' do - let(:params) { group_params.merge(allow_mfa_for_subgroups: false) } - let(:service) { described_class.new(user, params) } + context 'with `allow_mfa_for_subgroups` attribute' do + let(:extra_params) { { allow_mfa_for_subgroups: false } } - it 'creates group without error' do - expect(service.execute).to be_persisted - end + it_behaves_like 'creating a group' end - describe 'creating a top level group' do - let(:service) { described_class.new(user, group_params) } - + context 'for a top level group' do context 'when user can create a group' do before do user.update_attribute(:can_create_group, true) end - it { is_expected.to be_persisted } + it_behaves_like 'creating a group' context 'with before_commit callback' do it_behaves_like 'has sync-ed traversal_ids' @@ -115,187 +122,167 @@ RSpec.describe Groups::CreateService, '#execute', feature_category: :groups_and_ user.update_attribute(:can_create_group, false) end - it { is_expected.not_to be_persisted } + it_behaves_like 'does not create a group' end end - describe 'creating a group within an organization' do - let(:current_user) { user } - let(:service) { described_class.new(current_user, params) } - + context 'when creating a group within an organization' do context 'when organization is provided' do let_it_be(:organization) { create(:organization) } - let(:params) { group_params.merge(organization_id: organization.id) } + let(:extra_params) { { organization_id: organization.id } } context 'when user can create the group' do before do create(:organization_user, user: user, organization: organization) end - it { is_expected.to be_persisted } + it_behaves_like 'creating a group' end context 'when user is an admin', :enable_admin_mode do let(:current_user) { create(:admin) } - it { is_expected.to be_persisted } + it_behaves_like 'creating a group' end context 'when user can not create the group' do - it 'does not save group and returns an error' do - expect(execute).not_to be_persisted - expect(execute.errors[:organization_id].first) + it_behaves_like 'does not create a group' + + it 'returns an error and does not set organization_id' do + expect(created_group.errors[:organization_id].first) .to eq(s_("CreateGroup|You don't have permission to create a group in the provided organization.")) - expect(execute.organization_id).to be_nil + expect(created_group.organization_id).to be_nil end end end context 'when organization is the default organization and not set by params' do - let(:params) { group_params } - before do create(:organization, :default) end - it { is_expected.to be_persisted } + it_behaves_like 'creating a group' end end - describe 'creating subgroup' do - let!(:group) { create(:group) } - let!(:service) { described_class.new(user, group_params.merge(parent_id: group.id)) } + context 'for a subgroup' do + let_it_be(:group) { create(:group) } + let(:extra_params) { { parent_id: group.id } } context 'as group owner' do - before do + before_all do group.add_owner(user) end - it { is_expected.to be_persisted } - + it_behaves_like 'creating a group' it_behaves_like 'has sync-ed traversal_ids' end context 'as guest' do - it 'does not save group and returns an error' do - is_expected.not_to be_persisted + it_behaves_like 'does not create a group' - expect(subject.errors[:parent_id].first).to eq(s_('CreateGroup|You don’t have permission to create a subgroup in this group.')) - expect(subject.parent_id).to be_nil + it 'returns an error and does not set parent_id' do + expect(created_group.errors[:parent_id].first) + .to eq(s_('CreateGroup|You don’t have permission to create a subgroup in this group.')) + expect(created_group.parent_id).to be_nil end end context 'as owner' do - before do + before_all do group.add_owner(user) end - it { is_expected.to be_persisted } + it_behaves_like 'creating a group' end context 'as maintainer' do - before do + before_all do group.add_maintainer(user) end - it { is_expected.to be_persisted } + it_behaves_like 'creating a group' end end - describe "when visibility level is passed as a string" do - let(:service) { described_class.new(user, group_params) } - let(:group_params) { { path: 'group_path', visibility: 'public' } } - - it "assigns the correct visibility level" do - group = service.execute + context 'when visibility level is passed as a string' do + let(:extra_params) { { visibility: 'public' } } - expect(group.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) + it 'assigns the correct visibility level' do + expect(created_group.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) end end - describe 'creating a mattermost team' do - let!(:params) { group_params.merge(create_chat_team: "true") } - let!(:service) { described_class.new(user, params) } + context 'for creating a mattermost team' do + let(:extra_params) { { create_chat_team: 'true' } } before do stub_mattermost_setting(enabled: true) end it 'create the chat team with the group' do - allow_any_instance_of(::Mattermost::Team).to receive(:create) - .and_return({ 'name' => 'tanuki', 'id' => 'lskdjfwlekfjsdifjj' }) + allow_next_instance_of(::Mattermost::Team) do |instance| + allow(instance).to receive(:create).and_return({ 'name' => 'tanuki', 'id' => 'lskdjfwlekfjsdifjj' }) + end - expect { subject }.to change { ChatTeam.count }.from(0).to(1) + expect { response }.to change { ChatTeam.count }.from(0).to(1) end end - describe 'creating a setting record' do - let(:service) { described_class.new(user, group_params) } - + context 'for creating a setting record' do it 'create the settings record connected to the group' do - group = subject - expect(group.namespace_settings).to be_persisted + expect(created_group.namespace_settings).to be_persisted end end - describe 'creating a details record' do - let(:service) { described_class.new(user, group_params) } - + context 'for creating a details record' do it 'create the details record connected to the group' do - group = subject - expect(group.namespace_details).to be_persisted + expect(created_group.namespace_details).to be_persisted end end - describe 'create service for the group' do - let(:service) { described_class.new(user, group_params) } - let(:created_group) { service.execute } + context 'with an active instance-level integration' do + let_it_be(:instance_integration) do + create(:prometheus_integration, :instance, api_url: 'https://prometheus.instance.com/') + end + + it 'creates a service from the instance-level integration' do + expect(created_group.integrations.count).to eq(1) + expect(created_group.integrations.first.api_url).to eq(instance_integration.api_url) + expect(created_group.integrations.first.inherit_from_id).to eq(instance_integration.id) + end - context 'with an active instance-level integration' do - let!(:instance_integration) { create(:prometheus_integration, :instance, api_url: 'https://prometheus.instance.com/') } + context 'with an active group-level integration' do + let(:extra_params) { { parent_id: group.id } } + let_it_be(:group) { create(:group) { |g| g.add_owner(user) } } + let_it_be(:group_integration) do + create(:prometheus_integration, :group, group: group, api_url: 'https://prometheus.group.com/') + end - it 'creates a service from the instance-level integration' do + it 'creates a service from the group-level integration' do expect(created_group.integrations.count).to eq(1) - expect(created_group.integrations.first.api_url).to eq(instance_integration.api_url) - expect(created_group.integrations.first.inherit_from_id).to eq(instance_integration.id) + expect(created_group.integrations.first.api_url).to eq(group_integration.api_url) + expect(created_group.integrations.first.inherit_from_id).to eq(group_integration.id) end - context 'with an active group-level integration' do - let(:service) { described_class.new(user, group_params.merge(parent_id: group.id)) } - let!(:group_integration) { create(:prometheus_integration, :group, group: group, api_url: 'https://prometheus.group.com/') } - let(:group) do - create(:group).tap do |group| - group.add_owner(user) - end + context 'with an active subgroup' do + let(:extra_params) { { parent_id: subgroup.id } } + let_it_be(:subgroup) { create(:group, parent: group) { |g| g.add_owner(user) } } + let_it_be(:subgroup_integration) do + create(:prometheus_integration, :group, group: subgroup, api_url: 'https://prometheus.subgroup.com/') end - it 'creates a service from the group-level integration' do + it 'creates a service from the subgroup-level integration' do expect(created_group.integrations.count).to eq(1) - expect(created_group.integrations.first.api_url).to eq(group_integration.api_url) - expect(created_group.integrations.first.inherit_from_id).to eq(group_integration.id) - end - - context 'with an active subgroup' do - let(:service) { described_class.new(user, group_params.merge(parent_id: subgroup.id)) } - let!(:subgroup_integration) { create(:prometheus_integration, :group, group: subgroup, api_url: 'https://prometheus.subgroup.com/') } - let(:subgroup) do - create(:group, parent: group).tap do |subgroup| - subgroup.add_owner(user) - end - end - - it 'creates a service from the subgroup-level integration' do - expect(created_group.integrations.count).to eq(1) - expect(created_group.integrations.first.api_url).to eq(subgroup_integration.api_url) - expect(created_group.integrations.first.inherit_from_id).to eq(subgroup_integration.id) - end + expect(created_group.integrations.first.api_url).to eq(subgroup_integration.api_url) + expect(created_group.integrations.first.inherit_from_id).to eq(subgroup_integration.id) end end end end - context 'shared runners configuration' do - context 'parent group present' do + context 'with shared runners configuration' do + context 'when parent group is present' do using RSpec::Parameterized::TableSyntax where(:shared_runners_config, :descendants_override_disabled_shared_runners_config) do @@ -306,30 +293,31 @@ RSpec.describe Groups::CreateService, '#execute', feature_category: :groups_and_ end with_them do - let!(:group) { create(:group, shared_runners_enabled: shared_runners_config, allow_descendants_override_disabled_shared_runners: descendants_override_disabled_shared_runners_config) } - let!(:service) { described_class.new(user, group_params.merge(parent_id: group.id)) } + let(:extra_params) { { parent_id: group.id } } + let(:group) do + create( + :group, + shared_runners_enabled: shared_runners_config, + allow_descendants_override_disabled_shared_runners: descendants_override_disabled_shared_runners_config + ) + end before do group.add_owner(user) end it 'creates group following the parent config' do - new_group = service.execute - - expect(new_group.shared_runners_enabled).to eq(shared_runners_config) - expect(new_group.allow_descendants_override_disabled_shared_runners).to eq(descendants_override_disabled_shared_runners_config) + expect(created_group.shared_runners_enabled).to eq(shared_runners_config) + expect(created_group.allow_descendants_override_disabled_shared_runners) + .to eq(descendants_override_disabled_shared_runners_config) end end end - context 'root group' do - let!(:service) { described_class.new(user) } - + context 'for root group' do it 'follows default config' do - new_group = service.execute - - expect(new_group.shared_runners_enabled).to eq(true) - expect(new_group.allow_descendants_override_disabled_shared_runners).to eq(false) + expect(created_group.shared_runners_enabled).to eq(true) + expect(created_group.allow_descendants_override_disabled_shared_runners).to eq(false) end end end diff --git a/spec/support/helpers/cycle_analytics_helpers.rb b/spec/support/helpers/cycle_analytics_helpers.rb index 3f04d984e55..d54dcc8a31d 100644 --- a/spec/support/helpers/cycle_analytics_helpers.rb +++ b/spec/support/helpers/cycle_analytics_helpers.rb @@ -14,12 +14,14 @@ module CycleAnalyticsHelpers page.all('.gl-path-button').collect(&:text).map { |name_with_median| name_with_median.split("\n")[0] } end - def fill_in_custom_stage_fields + def fill_in_custom_stage_fields(stage_name = nil) index = page.all('[data-testid="value-stream-stage-fields"]').length last_stage = page.all('[data-testid="value-stream-stage-fields"]').last + stage_name = "Cool custom stage - name #{index}" if stage_name.blank? + within last_stage do - find('[name*="custom-stage-name-"]').fill_in with: "Cool custom stage - name #{index}" + find('[name*="custom-stage-name-"]').fill_in with: stage_name select_dropdown_option_by_value "custom-stage-start-event-", 'Merge request created' select_dropdown_option_by_value "custom-stage-end-event-", 'Merge request merged' end diff --git a/spec/support/shared_examples/features/work_items_shared_examples.rb b/spec/support/shared_examples/features/work_items_shared_examples.rb index 7ff37030493..0f35681ca7d 100644 --- a/spec/support/shared_examples/features/work_items_shared_examples.rb +++ b/spec/support/shared_examples/features/work_items_shared_examples.rb @@ -1,16 +1,20 @@ # frozen_string_literal: true +RSpec.shared_context 'with work_items_mvc_2' do |flag| + before do + stub_feature_flags(work_items_mvc_2: flag) + + page.refresh + wait_for_all_requests + end +end + RSpec.shared_examples 'work items title' do let(:title_selector) { '[data-testid="work-item-title"]' } let(:title_with_edit_selector) { '[data-testid="work-item-title-with-edit"]' } context 'when the work_items_mvc_2 FF is disabled' do - before do - stub_feature_flags(work_items_mvc_2: false) - - page.refresh - wait_for_all_requests - end + include_context 'with work_items_mvc_2', false it 'successfully shows and changes the title of the work item' do expect(work_item.reload.title).to eq work_item.title @@ -24,17 +28,12 @@ RSpec.shared_examples 'work items title' do end context 'when the work_items_mvc_2 FF is enabled' do - before do - stub_feature_flags(work_items_mvc_2: true) - - page.refresh - wait_for_all_requests - end + include_context 'with work_items_mvc_2', true it 'successfully shows and changes the title of the work item' do expect(work_item.reload.title).to eq work_item.title - click_button 'Edit' + click_button 'Edit', match: :first find(title_with_edit_selector).set("Work item title") send_keys([:command, :enter]) wait_for_requests @@ -333,15 +332,10 @@ RSpec.shared_examples 'work items description' do [true, false].each do |work_items_mvc_2_flag| # rubocop:disable RSpec/UselessDynamicDefinition -- check it for both off and on let(:edit_button) { work_items_mvc_2_flag ? 'Edit' : 'Edit description' } - before do - stub_feature_flags(work_items_mvc_2: work_items_mvc_2_flag) - - page.refresh - wait_for_all_requests - end + include_context 'with work_items_mvc_2', work_items_mvc_2_flag it 'shows GFM autocomplete', :aggregate_failures do - click_button edit_button + click_button edit_button, match: :first fill_in _('Description'), with: "@#{user.username}" page.within('.atwho-container') do @@ -350,7 +344,7 @@ RSpec.shared_examples 'work items description' do end it 'autocompletes available quick actions', :aggregate_failures do - click_button edit_button + click_button edit_button, match: :first fill_in _('Description'), with: '/' page.within('#at-view-commands') do @@ -371,7 +365,7 @@ RSpec.shared_examples 'work items description' do end it 'shows conflict message when description changes', :aggregate_failures do - click_button edit_button + click_button edit_button, match: :first ::WorkItems::UpdateService.new( container: work_item.project, @@ -411,17 +405,61 @@ RSpec.shared_examples 'work items invite members' do end RSpec.shared_examples 'work items milestone' do - it 'searches and sets or removes milestone for the work item' do - click_button s_('WorkItem|Add to milestone') - send_keys "\"#{milestone.title}\"" - select_listbox_item(milestone.title, exact_text: true) + context 'on work_items_mvc_2 FF off' do + include_context 'with work_items_mvc_2', false + + it 'searches and sets or removes milestone for the work item' do + click_button s_('WorkItem|Add to milestone') + send_keys "\"#{milestone.title}\"" + select_listbox_item(milestone.title, exact_text: true) + + expect(page).to have_button(milestone.title) + + click_button milestone.title + select_listbox_item(s_('WorkItem|No milestone'), exact_text: true) + + expect(page).to have_button(s_('WorkItem|Add to milestone')) + end + end - expect(page).to have_button(milestone.title) + context 'on work_items_mvc_2 FF on' do + let(:work_item_milestone_selector) { '[data-testid="work-item-milestone-with-edit"]' } - click_button milestone.title - select_listbox_item(s_('WorkItem|No milestone'), exact_text: true) + include_context 'with work_items_mvc_2', true - expect(page).to have_button(s_('WorkItem|Add to milestone')) + it 'passes axe automated accessibility testing in closed state' do + expect(page).to be_axe_clean.within(work_item_milestone_selector) + end + + context 'when edit is clicked' do + it 'selects and updates the right milestone', :aggregate_failures do + find_and_click_edit(work_item_milestone_selector) + + select_listbox_item(milestones[10].title) + + wait_for_requests + within(work_item_milestone_selector) do + expect(page).to have_text(milestones[10].title) + end + + find_and_click_edit(work_item_milestone_selector) + + find_and_click_clear(work_item_milestone_selector) + + expect(find(work_item_milestone_selector)).to have_content('None') + end + + it 'searches and sets or removes milestone for the work item' do + find_and_click_edit(work_item_milestone_selector) + within(work_item_milestone_selector) do + send_keys "\"#{milestones[11].title}\"" + wait_for_requests + + select_listbox_item(milestones[11].title) + expect(page).to have_text(milestones[11].title) + end + end + end end end @@ -603,12 +641,7 @@ RSpec.shared_examples 'work items iteration' do ) end - before do - stub_feature_flags(work_items_mvc_2: true) - - page.refresh - wait_for_all_requests - end + include_context 'with work_items_mvc_2', true context 'for accessibility' do it 'has the work item iteration with edit' do diff --git a/workhorse/go.mod b/workhorse/go.mod index 099e3723193..30a228219a5 100644 --- a/workhorse/go.mod +++ b/workhorse/go.mod @@ -6,7 +6,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.1 github.com/BurntSushi/toml v1.3.2 github.com/alecthomas/chroma/v2 v2.12.0 - github.com/aws/aws-sdk-go v1.48.11 + github.com/aws/aws-sdk-go v1.49.3 github.com/disintegration/imaging v1.6.2 github.com/getsentry/raven-go v0.2.0 github.com/golang-jwt/jwt/v5 v5.2.0 diff --git a/workhorse/go.sum b/workhorse/go.sum index c203d78a738..33bfccceb71 100644 --- a/workhorse/go.sum +++ b/workhorse/go.sum @@ -94,8 +94,8 @@ github.com/alecthomas/chroma/v2 v2.12.0/go.mod h1:4TQu7gdfuPjSh76j78ietmqh9LiurG github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/aws/aws-sdk-go v1.44.256/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= -github.com/aws/aws-sdk-go v1.48.11 h1:9YbiSbaF/jWi+qLRl+J5dEhr2mcbDYHmKg2V7RBcD5M= -github.com/aws/aws-sdk-go v1.48.11/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= +github.com/aws/aws-sdk-go v1.49.3 h1:+UGwhC3kChk0pRCxSsbaQSNIc8MfFURQL44Ig6RRR3I= +github.com/aws/aws-sdk-go v1.49.3/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= github.com/aws/aws-sdk-go-v2 v1.23.1 h1:qXaFsOOMA+HsZtX8WoCa+gJnbyW7qyFFBlPqvTSzbaI= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.1 h1:ZY3108YtBNq96jNZTICHxN1gSBSbnvIdYwwqnvCV4Mc= github.com/aws/aws-sdk-go-v2/config v1.25.5 h1:UGKm9hpQS2hoK8CEJ1BzAW8NbUpvwDJJ4lyqXSzu8bk= |