Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-05-23 12:10:24 +00:00
parent 611b009e92
commit e6d048d769
60 changed files with 729 additions and 346 deletions

View File

@ -96,7 +96,7 @@ review-build-cng:
variables:
HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}"
DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}"
GITLAB_HELM_CHART_REF: "febc4ad69acb7bba0eeb4a62daa577d0b7c3ee71" # 6.9.1: https://gitlab.com/gitlab-org/charts/gitlab/-/commit/febc4ad69acb7bba0eeb4a62daa577d0b7c3ee71
GITLAB_HELM_CHART_REF: "b0d2cc33afaa29b6e28e3b2b3591239fad8377a0" # 6.10.7: https://gitlab.com/gitlab-org/charts/gitlab/-/commit/b0d2cc33afaa29b6e28e3b2b3591239fad8377a0
environment:
name: review/${CI_COMMIT_REF_SLUG}${SCHEDULE_TYPE} # No separator for SCHEDULE_TYPE so it's compatible as before and looks nice without it
url: https://gitlab-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}

View File

@ -452,10 +452,10 @@ group :test do
gem 'rspec-benchmark', '~> 0.6.0'
gem 'rspec-parameterized', '~> 1.0', require: false
gem 'capybara', '~> 3.39'
gem 'capybara', '~> 3.39', '>= 3.39.1'
gem 'capybara-screenshot', '~> 1.0.26'
# 4.9.1 drops Ruby 2.7 support. We can upgrade further after we drop Ruby 2.7 support.
gem 'selenium-webdriver', '= 4.9.0'
gem 'selenium-webdriver', '= 4.9.1'
gem 'graphlyte', '~> 1.0.0'

View File

@ -67,7 +67,7 @@
{"name":"bullet","version":"7.0.2","platform":"ruby","checksum":"4b7986b366f694bb05d5c1b4ea8ba949a99224d4511bf02f0c3944112f719c81"},
{"name":"bundler-audit","version":"0.7.0.1","platform":"ruby","checksum":"12d853cb0b92fa8868abbb539414d7a33da9e48b792e2ff28271d36c8ace8912"},
{"name":"byebug","version":"11.1.3","platform":"ruby","checksum":"2485944d2bb21283c593d562f9ae1019bf80002143cc3a255aaffd4e9cf4a35b"},
{"name":"capybara","version":"3.39.0","platform":"ruby","checksum":"a30994beb4b4f318e39e3dc81e73203bd1edf1f9ef237d82b708eb1c21b56419"},
{"name":"capybara","version":"3.39.1","platform":"ruby","checksum":"25831b3860d54b88013e6e41b77412b2e6a80bcc59aa9a1d48b0f8de65210fe2"},
{"name":"capybara-screenshot","version":"1.0.26","platform":"ruby","checksum":"816b9370a07752097c82a05f568aaf5d3b7f45c3db5d3aab2014071e1b3c0c77"},
{"name":"carrierwave","version":"1.3.3","platform":"ruby","checksum":"0f0244de2ece54c80745b755993bd26cf47d4650823e5f89c115dbc9d73a13f1"},
{"name":"cbor","version":"0.5.9.6","platform":"ruby","checksum":"434a147658dd1df24ec9e7b3297c1fd4f8a691c97d0e688b3049df8e728b2114"},
@ -563,7 +563,7 @@
{"name":"sawyer","version":"0.9.2","platform":"ruby","checksum":"fa3a72d62a4525517b18857ddb78926aab3424de0129be6772a8e2ba240e7aca"},
{"name":"sd_notify","version":"0.1.1","platform":"ruby","checksum":"cbc7ac6caa7cedd26b30a72b5eeb6f36050dc0752df263452ea24fb5a4ad3131"},
{"name":"seed-fu","version":"2.3.7","platform":"ruby","checksum":"f19673443e9af799b730e3d4eca6a89b39e5a36825015dffd00d02ea3365cf74"},
{"name":"selenium-webdriver","version":"4.9.0","platform":"ruby","checksum":"0f5fc4118ab231e5ef1895b1e14a4366eb9d73d60a8e42b0d84f69cdfdd8b6cf"},
{"name":"selenium-webdriver","version":"4.9.1","platform":"ruby","checksum":"055b8c3a528c7150d7e1f6b8551725f7643a8c00f36028a052f6ec8e50819184"},
{"name":"semver_dialects","version":"1.2.1","platform":"ruby","checksum":"60a1f67659f79c51a667e8858ec9b089c1e4ce4f6d2a0f0b4ac101916946eb23"},
{"name":"sentry-rails","version":"5.8.0","platform":"ruby","checksum":"c11b2d909de2c2bfda793c45f64180fd784d54c46886338b683ee3f8efa7731b"},
{"name":"sentry-raven","version":"3.1.2","platform":"ruby","checksum":"103d3b122958810d34898ce2e705bcf549ddb9d855a70ce9a3970ee2484f364a"},

View File

@ -266,7 +266,7 @@ GEM
bundler (>= 1.2.0, < 3)
thor (>= 0.18, < 2)
byebug (11.1.3)
capybara (3.39.0)
capybara (3.39.1)
addressable
matrix
mini_mime (>= 0.1.3)
@ -1392,7 +1392,7 @@ GEM
seed-fu (2.3.7)
activerecord (>= 3.1)
activesupport (>= 3.1)
selenium-webdriver (4.9.0)
selenium-webdriver (4.9.1)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
@ -1684,7 +1684,7 @@ DEPENDENCIES
bullet (~> 7.0.2)
bundler-audit (~> 0.7.0.1)
bundler-checksum (~> 0.1.0)!
capybara (~> 3.39)
capybara (~> 3.39, >= 3.39.1)
capybara-screenshot (~> 1.0.26)
carrierwave (~> 1.3)
charlock_holmes (~> 0.7.7)
@ -1906,7 +1906,7 @@ DEPENDENCIES
sassc-rails (~> 2.1.0)
sd_notify (~> 0.1.0)
seed-fu (~> 2.3.7)
selenium-webdriver (= 4.9.0)
selenium-webdriver (= 4.9.1)
semver_dialects (~> 1.2.1)
sentry-rails (~> 5.8.0)
sentry-raven (~> 3.1)

View File

@ -325,7 +325,7 @@ export default {
this.adjustView();
},
viewDiffsFileByFile(newViewFileByFile) {
if (!newViewFileByFile && this.diffsIncomplete && this.glFeatures.singleFileFileByFile) {
if (!newViewFileByFile && this.diffsIncomplete) {
this.refetchDiffData({ refetchMeta: false });
}
},
@ -467,26 +467,19 @@ export default {
subscribeToEvents() {
notesEventHub.$once('fetchDiffData', this.fetchData);
notesEventHub.$on('refetchDiffData', this.refetchDiffData);
if (this.glFeatures.singleFileFileByFile) {
diffsEventHub.$on('diffFilesModified', this.setDiscussions);
notesEventHub.$on('fetchedNotesData', this.rereadNoteHash);
}
notesEventHub.$on('fetchedNotesData', this.rereadNoteHash);
diffsEventHub.$on('diffFilesModified', this.setDiscussions);
diffsEventHub.$on(EVT_MR_PREPARED, this.fetchData);
},
unsubscribeFromEvents() {
diffsEventHub.$off(EVT_MR_PREPARED, this.fetchData);
if (this.glFeatures.singleFileFileByFile) {
notesEventHub.$off('fetchedNotesData', this.rereadNoteHash);
diffsEventHub.$off('diffFilesModified', this.setDiscussions);
}
diffsEventHub.$off('diffFilesModified', this.setDiscussions);
notesEventHub.$off('fetchedNotesData', this.rereadNoteHash);
notesEventHub.$off('refetchDiffData', this.refetchDiffData);
notesEventHub.$off('fetchDiffData', this.fetchData);
},
navigateToDiffFileNumber(number) {
this.navigateToDiffFileIndex({
index: number - 1,
singleFile: this.glFeatures.singleFileFileByFile,
});
this.navigateToDiffFileIndex(number - 1);
},
refetchDiffData({ refetchMeta = true } = {}) {
this.fetchData({ toggleTree: false, fetchMeta: refetchMeta });
@ -506,7 +499,7 @@ export default {
if (data) {
realSize = data.real_size;
if (this.viewDiffsFileByFile && this.glFeatures.singleFileFileByFile) {
if (this.viewDiffsFileByFile) {
this.fetchFileByFile();
}
}
@ -527,7 +520,7 @@ export default {
});
}
if (!this.viewDiffsFileByFile || !this.glFeatures.singleFileFileByFile) {
if (!this.viewDiffsFileByFile) {
this.fetchDiffFilesBatch()
.then(() => {
if (toggleTree) this.setTreeDisplay();
@ -618,10 +611,7 @@ export default {
jumpToFile(step) {
const targetIndex = this.currentDiffIndex + step;
if (targetIndex >= 0 && targetIndex < this.flatBlobsList.length) {
this.goToFile({
path: this.flatBlobsList[targetIndex].path,
singleFile: this.glFeatures.singleFileFileByFile,
});
this.goToFile({ path: this.flatBlobsList[targetIndex].path });
}
},
setTreeDisplay() {

View File

@ -209,12 +209,6 @@ export default {
if (this.hasDiff) {
this.postRender();
} else if (
this.viewDiffsFileByFile &&
!this.isCollapsed &&
!this.glFeatures.singleFileFileByFile
) {
this.requestDiff();
}
this.manageViewedEffects();

View File

@ -5,7 +5,6 @@ import micromatch from 'micromatch';
import { debounce } from 'lodash';
import { getModifierKey } from '~/constants';
import { s__, sprintf } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { RecycleScroller } from 'vendor/vue-virtual-scroller';
import DiffFileRow from './diff_file_row.vue';
@ -20,7 +19,6 @@ export default {
DiffFileRow,
RecycleScroller,
},
mixins: [glFeatureFlagsMixin()],
props: {
hideFileStats: {
type: Boolean,
@ -177,7 +175,7 @@ export default {
:class="{ 'tree-list-parent': item.level > 0 }"
class="gl-relative"
@toggleTreeOpen="toggleTreeOpen"
@clickFile="(path) => goToFile({ singleFile: glFeatures.singleFileFileByFile, path })"
@clickFile="(path) => goToFile({ path })"
/>
</template>
<template #after>

View File

@ -608,8 +608,8 @@ export const setCurrentFileHash = ({ commit }, hash) => {
commit(types.SET_CURRENT_DIFF_FILE, hash);
};
export const goToFile = ({ state, commit, dispatch, getters }, { path, singleFile }) => {
if (!state.viewDiffsFileByFile || !singleFile) {
export const goToFile = ({ state, commit, dispatch, getters }, { path }) => {
if (!state.viewDiffsFileByFile) {
dispatch('scrollToFile', { path });
} else {
if (!state.treeEntries[path]) return;
@ -943,16 +943,13 @@ export const setCurrentDiffFileIdFromNote = ({ commit, getters, rootGetters }, n
}
};
export const navigateToDiffFileIndex = (
{ state, getters, commit, dispatch },
{ index, singleFile },
) => {
export const navigateToDiffFileIndex = ({ state, getters, commit, dispatch }, index) => {
const { fileHash } = getters.flatBlobsList[index];
document.location.hash = fileHash;
commit(types.SET_CURRENT_DIFF_FILE, fileHash);
if (state.viewDiffsFileByFile && singleFile) {
if (state.viewDiffsFileByFile) {
dispatch('fetchFileByFile');
}
};

View File

@ -14,6 +14,7 @@ export const TYPE_EPIC = 'epic';
export const TYPE_INCIDENT = 'incident';
export const TYPE_ISSUE = 'issue';
export const TYPE_MERGE_REQUEST = 'merge_request';
export const TYPE_MILESTONE = 'milestone';
export const TYPE_TEST_CASE = 'test_case';
export const WORKSPACE_GROUP = 'group';

View File

@ -9,12 +9,18 @@ import Sidebar from '~/right_sidebar';
import MountMilestoneSidebar from '~/sidebar/mount_milestone_sidebar';
import Translate from '~/vue_shared/translate';
import ZenMode from '~/zen_mode';
import TaskList from '~/task_list';
import { TYPE_MILESTONE } from '~/issues/constants';
import { createAlert } from '~/alert';
import { __ } from '~/locale';
import DeleteMilestoneModal from './components/delete_milestone_modal.vue';
import PromoteMilestoneModal from './components/promote_milestone_modal.vue';
import eventHub from './event_hub';
// See app/views/shared/milestones/_description.html.haml
export const MILESTONE_DESCRIPTION_ELEMENT = '.milestone-detail .description';
export const MILESTONE_DESCRIPTION_TASK_LIST_CONTAINER_ELEMENT = `${MILESTONE_DESCRIPTION_ELEMENT}.js-task-list-container`;
export const MILESTONE_DETAIL_ELEMENT = '.milestone-detail';
export function initForm(initGFM = true) {
new ZenMode(); // eslint-disable-line no-new
@ -40,6 +46,26 @@ export function initShow() {
new MountMilestoneSidebar(); // eslint-disable-line no-new
renderGFM(document.querySelector(MILESTONE_DESCRIPTION_ELEMENT));
const el = document.querySelector(MILESTONE_DESCRIPTION_TASK_LIST_CONTAINER_ELEMENT);
if (!el) {
return null;
}
return new TaskList({
dataType: TYPE_MILESTONE,
fieldName: 'description',
selector: MILESTONE_DETAIL_ELEMENT,
lockVersion: el.dataset.lockVersion,
onError: () => {
createAlert({
message: __(
'Someone edited this milestone at the same time you did. Please refresh the page to see changes.',
),
});
},
});
}
export function initPromoteMilestoneModal() {

View File

@ -1,18 +1,12 @@
<script>
import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
import { GlCollapsibleListbox } from '@gitlab/ui';
import { __ } from '~/locale';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import { formatTimezone } from '~/lib/utils/datetime_utility';
export default {
name: 'TimezoneDropdown',
components: {
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
},
directives: {
autofocusonshow,
GlCollapsibleListbox,
},
props: {
value: {
@ -52,11 +46,10 @@ export default {
identifier: timezone.identifier,
}));
},
filteredResults() {
const lowerCasedSearchTerm = this.searchTerm.toLowerCase();
return this.timezones.filter((timezone) =>
timezone.formattedTimezone.toLowerCase().includes(lowerCasedSearchTerm),
);
filteredListboxItems() {
return this.timezones
.filter((timezone) => timezone.formattedTimezone.toLowerCase().includes(this.searchTerm))
.map(({ formattedTimezone }) => ({ value: formattedTimezone, text: formattedTimezone }));
},
selectedTimezoneLabel() {
return this.tzValue || __('Select timezone');
@ -68,14 +61,14 @@ export default {
},
},
methods: {
selectTimezone(selectedTimezone) {
this.tzValue = selectedTimezone.formattedTimezone;
selectTimezone(formattedTimezone) {
const selectedTimezone = this.timezones.find(
(timezone) => timezone.formattedTimezone === formattedTimezone,
);
this.tzValue = formattedTimezone;
this.$emit('input', selectedTimezone);
this.searchTerm = '';
},
isSelected(timezone) {
return this.tzValue === timezone.formattedTimezone;
},
initialTimezone(timezones, value) {
if (!value) {
return undefined;
@ -89,6 +82,9 @@ export default {
return undefined;
},
setSearchTerm(value) {
this.searchTerm = value?.toLowerCase();
},
},
};
</script>
@ -101,31 +97,16 @@ export default {
:value="timezoneIdentifier || value"
type="hidden"
/>
<gl-dropdown
:text="selectedTimezoneLabel"
:class="additionalClass"
<gl-collapsible-listbox
:items="filteredListboxItems"
:toggle-text="selectedTimezoneLabel"
:toggle-class="additionalClass"
:no-results-text="$options.translations.noResultsText"
:selected="tzValue"
block
lazy
menu-class="gl-w-full!"
v-bind="$attrs"
>
<gl-search-box-by-type v-model.trim="searchTerm" v-autofocusonshow autofocus />
<gl-dropdown-item
v-for="timezone in filteredResults"
:key="timezone.formattedTimezone"
:is-checked="isSelected(timezone)"
is-check-item
@click="selectTimezone(timezone)"
>
{{ timezone.formattedTimezone }}
</gl-dropdown-item>
<gl-dropdown-item
v-if="!filteredResults.length"
class="gl-pointer-events-none"
data-testid="noMatchingResults"
>
{{ $options.translations.noResultsText }}
</gl-dropdown-item>
</gl-dropdown>
searchable
@search="setSearchTerm"
@select="selectTimezone"
/>
</div>
</template>

View File

@ -44,7 +44,19 @@ class Groups::MilestonesController < Groups::ApplicationController
def update
Milestones::UpdateService.new(@milestone.parent, current_user, milestone_params).execute(@milestone)
redirect_to milestone_path(@milestone)
respond_to do |format|
format.html do
redirect_to milestone_path(@milestone)
end
format.json do
if @milestone.valid?
head :no_content
else
render json: { errors: @milestone.errors.full_messages }, status: :unprocessable_entity
end
end
end
rescue ActiveRecord::StaleObjectError
respond_to do |format|
format.html do

View File

@ -44,7 +44,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:refactor_security_extension, @project)
push_frontend_feature_flag(:deprecate_vulnerabilities_feedback, @project)
push_frontend_feature_flag(:moved_mr_sidebar, project)
push_frontend_feature_flag(:single_file_file_by_file, project)
push_frontend_feature_flag(:mr_experience_survey, project)
push_frontend_feature_flag(:realtime_mr_status_change, project)
push_frontend_feature_flag(:realtime_approvals, project)

View File

@ -76,7 +76,6 @@ class Projects::MilestonesController < Projects::ApplicationController
@milestone = Milestones::UpdateService.new(project, current_user, milestone_params).execute(milestone)
respond_to do |format|
format.js
format.html do
if @milestone.valid?
redirect_to project_milestone_path(@project, @milestone)
@ -84,6 +83,16 @@ class Projects::MilestonesController < Projects::ApplicationController
render :edit
end
end
format.js
format.json do
if @milestone.valid?
head :no_content
else
render json: { errors: @milestone.errors.full_messages }, status: :unprocessable_entity
end
end
end
rescue ActiveRecord::StaleObjectError
respond_to do |format|

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
# Finder for retrieving organizations scoped to a group
# Finder for retrieving crm_organizations scoped to a group
#
# Arguments:
# current_user - user performing the action. Must have the correct permission level for the group.
@ -29,22 +29,22 @@ module Crm
def execute
return CustomerRelations::Organization.none unless root_group
organizations = root_group.organizations
organizations = by_ids(organizations)
organizations = by_search(organizations)
organizations = by_state(organizations)
sort_organizations(organizations)
crm_organizations = root_group.crm_organizations
crm_organizations = by_ids(crm_organizations)
crm_organizations = by_search(crm_organizations)
crm_organizations = by_state(crm_organizations)
sort_crm_organizations(crm_organizations)
end
private
def sort_organizations(organizations)
return organizations.sort_by_name unless @params.key?(:sort)
return organizations if @params[:sort].nil?
def sort_crm_organizations(crm_organizations)
return crm_organizations.sort_by_name unless @params.key?(:sort)
return crm_organizations if @params[:sort].nil?
field = @params[:sort][:field]
direction = @params[:sort][:direction]
organizations.sort_by_field(field, direction)
crm_organizations.sort_by_field(field, direction)
end
def root_group
@ -57,22 +57,22 @@ module Crm
end
end
def by_search(organizations)
return organizations unless search?
def by_search(crm_organizations)
return crm_organizations unless search?
organizations.search(params[:search])
crm_organizations.search(params[:search])
end
def by_state(organizations)
return organizations unless state?
def by_state(crm_organizations)
return crm_organizations unless state?
organizations.search_by_state(params[:state])
crm_organizations.search_by_state(params[:state])
end
def by_ids(organizations)
return organizations unless ids?
def by_ids(crm_organizations)
return crm_organizations unless ids?
organizations.id_in(params[:ids])
crm_organizations.id_in(params[:ids])
end
def search?

View File

@ -1,23 +1,22 @@
# frozen_string_literal: true
module SafeFormatHelper
# Returns a HTML-safe string where +format+ and +args+ are escaped via
# `html_escape` if they are not marked as HTML-safe.
#
# Argument +format+ must not be marked as HTML-safe via `.html_safe`.
# Returns a HTML-safe string where
# * +format+ is escaped via `html_escape_once`
# * +args+ are escaped via `html_escape` if they are not marked as HTML-safe
#
# Example:
# safe_format('Some %{open}bold%{close} text.', open: '<strong>'.html_safe, close: '</strong>'.html_safe)
# # => 'Some <strong>bold</strong>'
# safe_format('See %{user_input}', user_input: '<b>bold</b>')
# # => 'See &lt;b&gt;bold&lt;/b&gt;
# safe_format('In &lt; hour & more')
# # => 'In &lt; hour &amp; more'
#
def safe_format(format, **args)
raise ArgumentError, 'Argument `format` must not be marked as html_safe!' if format.html_safe?
# Use `Kernel.format` to avoid conflicts with ViewComponent's `format`.
Kernel.format(
html_escape(format),
html_escape_once(format),
args.transform_values { |value| html_escape(value) }
).html_safe
end

View File

@ -176,6 +176,11 @@ module Emails
.gsub(/%\{\s*SYSTEM_FOOTER\s*\}/, text_footer_message.to_s)
.gsub(/%\{\s*UNSUBSCRIBE_URL\s*\}/, unsubscribe_sent_notification_url(@sent_notification))
.gsub(/%\{\s*ADDITIONAL_TEXT\s*\}/, service_desk_email_additional_text.to_s)
.gsub(/%\{\s*ISSUE_URL\s*\}/, full_issue_url)
end
def full_issue_url
issue_url(@issue)
end
def issue_id

View File

@ -92,7 +92,7 @@ class Group < Namespace
has_many :badges, class_name: 'GroupBadge'
# AR defaults to nullify when trying to delete via has_many associations unless we set dependent: :delete_all
has_many :organizations, class_name: 'CustomerRelations::Organization', inverse_of: :group, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :crm_organizations, class_name: 'CustomerRelations::Organization', inverse_of: :group, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :contacts, class_name: 'CustomerRelations::Contact', inverse_of: :group, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :cluster_groups, class_name: 'Clusters::Group'

View File

@ -69,7 +69,7 @@ module Groups
return false if group.root_ancestor == @new_parent_group.root_ancestor
return true if group.contacts.exists? && !current_user.can?(:admin_crm_contact, @new_parent_group.root_ancestor)
return true if group.organizations.exists? && !current_user.can?(:admin_crm_organization, @new_parent_group.root_ancestor)
return true if group.crm_organizations.exists? && !current_user.can?(:admin_crm_organization, @new_parent_group.root_ancestor)
false
end

View File

@ -16,7 +16,7 @@
- add_page_specific_style 'page_bundles/ci_status'
- add_page_startup_api_call @endpoint_metadata_url
- if mr_action == 'diffs' && (!@file_by_file_default || !single_file_file_by_file?)
- if mr_action == 'diffs' && !@file_by_file_default
- add_page_startup_api_call @endpoint_diff_batch_url
.merge-request{ data: { mr_action: mr_action, url: merge_request_path(@merge_request, format: :json), project_path: project_path(@merge_request.project), lock_version: @merge_request.lock_version, diffs_batch_cache_key: @diffs_batch_cache_key } }

View File

@ -9,5 +9,7 @@
- if milestone.try(:description).present?
%div{ data: { qa_selector: "milestone_description_content" } }
.description.md.gl-px-0.gl-pt-4
.description.md.gl-px-0.gl-pt-4{ class: ('js-task-list-container' if can?(current_user, :admin_milestone, milestone)), data: { lock_version: @milestone.lock_version } }
= markdown_field(milestone, :description)
-# This textarea is necessary for `task_list.js` to work.
%textarea.hidden.js-task-list-field{ data: { value: milestone.description, update_url: milestone_path(milestone, format: :json)} }

View File

@ -1,8 +1,8 @@
---
name: bitbucket_server_user_mapping_by_username
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36885
rollout_issue_url:
rollout_issue_url: # No rollout: This is an ops-flag
milestone: '13.4'
type: development
type: ops
group: group::import
default_enabled: false
default_enabled: false # Flag should be kept disabled by default

View File

@ -0,0 +1,8 @@
---
name: code_suggestions_tokens_api
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120892
rollout_issue_url:
milestone: '16.1'
type: ops
group: group::ai assisted
default_enabled: true

View File

@ -6,6 +6,8 @@
stage: Enablement
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/387898
body: |
This deprecation is now superseded by another [deprecation notice](#running-a-single-database-is-deprecated).
Previously, [GitLab's database](https://docs.gitlab.com/omnibus/settings/database.html)
configuration had a single `main:` section. This is being deprecated. The new
configuration has both a `main:` and a `ci:` section.
@ -14,6 +16,4 @@
to [add the `ci:` section](https://docs.gitlab.com/ee/install/installation.html#configure-gitlab-db-settings).
Omnibus, the Helm chart, and Operator will handle this configuration
automatically from GitLab 16.0 onwards.
This change is a preparation to deprecate two connections in favor of two databases in 16.1.
documentation_url: https://docs.gitlab.com/ee/install/installation.html#configure-gitlab-db-settings

View File

@ -0,0 +1,9 @@
- title: "Running a single database is deprecated"
removal_milestone: "17.0"
announcement_milestone: "16.1"
breaking_change: true
reporter: lohrc
stage: data_stores
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/411239
body: |
The option to run self-managed installations of GitLab on a single database is now deprecated. From GitLab 17.0, we will require a [separate database for CI features](https://gitlab.com/groups/gitlab-org/-/epics/7509). With this change, self-managed versions of GitLab will behave similarly to GitLab.com. This change applies to installation methods with Omnibus GitLab, GitLab Helm chart, GitLab Operator, GitLab Docker images, and installation from source. Before upgrading to GitLab 17.0, please ensure [migration](https://docs.gitlab.com/ee/administration/postgresql/multiple_databases.html) to two databases.

View File

@ -0,0 +1,6 @@
---
migration_job_name: RemoveInvalidDeployAccessLevelGroups
description: This deletes protected_environment_deploy_access_levels rows that have invalid group_id.
feature_category: continuous_delivery
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121222
milestone: 16.1

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
class ScheduleToRemoveInvalidDeployAccessLevelGroups < Gitlab::Database::Migration[2.1]
MIGRATION = "RemoveInvalidDeployAccessLevelGroups"
DELAY_INTERVAL = 2.minutes
BATCH_SIZE = 1000
restrict_gitlab_migration gitlab_schema: :gitlab_main
def up
queue_batched_background_migration(
MIGRATION,
:protected_environment_deploy_access_levels,
:id,
job_interval: DELAY_INTERVAL,
batch_size: BATCH_SIZE
)
end
def down
delete_batched_background_migration(MIGRATION, :protected_environment_deploy_access_levels, :id, [])
end
end

View File

@ -0,0 +1 @@
c2340753bf27ef119dd76a49ada76f07ef6f22577ae11651e81bba6bd7502f08

View File

@ -9,7 +9,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
The Geo team performs manual testing and validation on common deployment configurations to ensure
that Geo works when upgrading between minor GitLab versions and major PostgreSQL database versions.
This section contains a journal of recent validation tests and links to the relevant issues.
This section contains a journal of validation tests and links to the relevant issues.
## GitLab upgrades
@ -184,24 +184,6 @@ The following are PostgreSQL upgrade validation tests we performed.
The following are additional validation tests we performed.
### May 2021
[Test failover with object storage replication enabled](https://gitlab.com/gitlab-org/gitlab/-/issues/330362):
- Description: At the time of testing, Geo's object storage replication functionality was in beta. We tested that object storage replication works as intended and that the data was present on the new primary after a failover.
- Outcome: The test was successful. Data in object storage was replicated and present after a failover.
- Follow up issues:
- [Geo: Failing to replicate initial Monitoring project](https://gitlab.com/gitlab-org/gitlab/-/issues/330485)
### January 2022
[Validate Object storage replication using Azure based object storage](https://gitlab.com/gitlab-org/gitlab/-/issues/348804#note_821294631):
- Description: Tested the average time it takes for a single image to replicate from the primary object storage location to the secondary when using Azure based object storage replication and [GitLab based object storage replication](object_storage.md#enabling-gitlab-managed-object-storage-replication). This was tested by uploading a 1 MB image to a project on the primary site every second for 60 seconds. The time was then measured until a image was available on the secondary site. This was achieved using a [Ruby Script](https://gitlab.com/gitlab-org/quality/geo-replication-tester).
- Outcome: When using Azure based replication the average time for an image to replicate from the primary object storage to the secondary was recorded as 40 seconds, the longest replication time was 70 seconds and the quickest was 11 seconds. When using GitLab based replication the average time for replication to complete was 5 seconds, the longest replication time was 10 seconds and the quickest was 3 seconds.
- Follow up issue:
- [Validate Cross Region Object storage replication using Azure based object storage](https://gitlab.com/gitlab-org/gitlab/-/issues/358154)
### April 2022
[Validate Object storage replication using AWS based object storage](https://gitlab.com/gitlab-org/gitlab/-/issues/351463):
@ -214,6 +196,24 @@ The following are additional validation tests we performed.
- Description: Tested the average time it takes for a single image to replicate from the primary object storage location to the secondary when using GCP based object storage replication and [GitLab based object storage replication](object_storage.md#enabling-gitlab-managed-object-storage-replication). This was tested by uploading a 1 MB image to a project on the primary site every second for 60 seconds. The time was then measured until a image was available on the secondary site. This was achieved using a [Ruby Script](https://gitlab.com/gitlab-org/quality/geo-replication-tester).
- Outcome: GCP handles replication differently than other Cloud Providers. In GCP, the process is to a create single bucket that is either multi, dual, or single region based. This means that the bucket automatically stores replicas in a region based on the option chosen. Even when using multi region, this only replicates in a single continent, the options being America, Europe, or Asia. At current there doesn't seem to be any way to replicate objects between continents using GCP based replication. For Geo managed replication the average time when replicating in the same region was 6 seconds, and when replicating cross region this rose to just 9 seconds.
### January 2022
[Validate Object storage replication using Azure based object storage](https://gitlab.com/gitlab-org/gitlab/-/issues/348804#note_821294631):
- Description: Tested the average time it takes for a single image to replicate from the primary object storage location to the secondary when using Azure based object storage replication and [GitLab based object storage replication](object_storage.md#enabling-gitlab-managed-object-storage-replication). This was tested by uploading a 1 MB image to a project on the primary site every second for 60 seconds. The time was then measured until a image was available on the secondary site. This was achieved using a [Ruby Script](https://gitlab.com/gitlab-org/quality/geo-replication-tester).
- Outcome: When using Azure based replication the average time for an image to replicate from the primary object storage to the secondary was recorded as 40 seconds, the longest replication time was 70 seconds and the quickest was 11 seconds. When using GitLab based replication the average time for replication to complete was 5 seconds, the longest replication time was 10 seconds and the quickest was 3 seconds.
- Follow up issue:
- [Validate Cross Region Object storage replication using Azure based object storage](https://gitlab.com/gitlab-org/gitlab/-/issues/358154)
### May 2021
[Test failover with object storage replication enabled](https://gitlab.com/gitlab-org/gitlab/-/issues/330362):
- Description: At the time of testing, Geo's object storage replication functionality was in beta. We tested that object storage replication works as intended and that the data was present on the new primary after a failover.
- Outcome: The test was successful. Data in object storage was replicated and present after a failover.
- Follow up issues:
- [Geo: Failing to replicate initial Monitoring project](https://gitlab.com/gitlab-org/gitlab/-/issues/330485)
## Other tests
### August 2020

View File

@ -589,7 +589,7 @@ instead:
- In Ruby/HAML:
```ruby
html_escape_once(_('In &lt; 1 hour')).html_safe
safe_format(_('In &lt; 1 hour'))
# => 'In < 1 hour'
```

View File

@ -479,6 +479,20 @@ that is available now. We recommend this alternative solution because it provide
<div class="deprecation breaking-change" data-milestone="17.0">
### Running a single database is deprecated
<div class="deprecation-notes">
- Announced in: GitLab <span class="milestone">16.1</span>
- This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/).
- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/411239).
</div>
The option to run self-managed installations of GitLab on a single database is now deprecated. From GitLab 17.0, we will require a [separate database for CI features](https://gitlab.com/groups/gitlab-org/-/epics/7509). With this change, self-managed versions of GitLab will behave similarly to GitLab.com. This change applies to installation methods with Omnibus GitLab, GitLab Helm chart, GitLab Operator, GitLab Docker images, and installation from source. Before upgrading to GitLab 17.0, please ensure [migration](https://docs.gitlab.com/ee/administration/postgresql/multiple_databases.html) to two databases.
</div>
<div class="deprecation breaking-change" data-milestone="17.0">
### Self-managed certificate-based integration with Kubernetes
<div class="deprecation-notes">
@ -513,6 +527,8 @@ For updates and details about this deprecation, follow [this epic](https://gitla
- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/387898).
</div>
This deprecation is now superseded by another [deprecation notice](#running-a-single-database-is-deprecated).
Previously, [GitLab's database](https://docs.gitlab.com/omnibus/settings/database.html)
configuration had a single `main:` section. This is being deprecated. The new
configuration has both a `main:` and a `ci:` section.
@ -522,8 +538,6 @@ to [add the `ci:` section](https://docs.gitlab.com/ee/install/installation.html#
Omnibus, the Helm chart, and Operator will handle this configuration
automatically from GitLab 16.0 onwards.
This change is a preparation to deprecate two connections in favor of two databases in 16.1.
</div>
<div class="deprecation breaking-change" data-milestone="17.0">

View File

@ -101,7 +101,8 @@ visible in the email template. For more information, see
#### Thank you email
> `%{ISSUE_DESCRIPTION}` [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/223751) in GitLab 16.0.
> - `%{ISSUE_DESCRIPTION}` [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/223751) in GitLab 16.0.
> - `%{ISSUE_URL}` [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/408793) in GitLab 16.1.
When a user submits an issue through Service Desk, GitLab sends a **thank you email**.
@ -110,20 +111,23 @@ directory in your repository, create a file named `thank_you.md`.
You can use these placeholders to be automatically replaced in each email:
- `%{ISSUE_ID}`: issue IID
- `%{ISSUE_PATH}`: project path appended with the issue IID
- `%{ISSUE_DESCRIPTION}`: issue description based on the original email
- `%{UNSUBSCRIBE_URL}`: unsubscribe URL
- `%{SYSTEM_HEADER}`: [system header message](../admin_area/appearance.md#system-header-and-footer-messages)
- `%{SYSTEM_FOOTER}`: [system footer message](../admin_area/appearance.md#system-header-and-footer-messages)
- `%{ADDITIONAL_TEXT}`: [custom additional text](../admin_area/settings/email.md#custom-additional-text)
- `%{ISSUE_ID}`: Issue IID.
- `%{ISSUE_PATH}`: Project path appended with the issue IID.
- `%{ISSUE_URL}`: URL to the issue. External participants can only view the issue if the project is public
and issue is not confidential (Service Desk issues are confidential by default).
- `%{ISSUE_DESCRIPTION}`: Issue description based on the original email.
- `%{UNSUBSCRIBE_URL}`: Unsubscribe URL.
- `%{SYSTEM_HEADER}`: [System header message](../admin_area/appearance.md#system-header-and-footer-messages).
- `%{SYSTEM_FOOTER}`: [System footer message](../admin_area/appearance.md#system-header-and-footer-messages).
- `%{ADDITIONAL_TEXT}`: [Custom additional text](../admin_area/settings/email.md#custom-additional-text).
Because Service Desk issues are created as [confidential](issues/confidential_issues.md) (only project members can see them),
the response email does not contain the issue link.
#### New note email
> `%{ISSUE_DESCRIPTION}` [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/223751) in GitLab 16.0.
> - `%{ISSUE_DESCRIPTION}` [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/223751) in GitLab 16.0.
> - `%{ISSUE_URL}` [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/408793) in GitLab 16.1.
When a user-submitted issue receives a new comment, GitLab sends a **new note email**.
@ -132,17 +136,19 @@ directory in your repository, create a file named `new_note.md`.
You can use these placeholders to be automatically replaced in each email:
- `%{ISSUE_ID}`: issue IID
- `%{ISSUE_PATH}`: project path appended with the issue IID
- `%{ISSUE_DESCRIPTION}`: issue description at the time email is generated.
- `%{ISSUE_ID}`: Issue IID.
- `%{ISSUE_PATH}`: Project path appended with the issue IID.
- `%{ISSUE_URL}`: URL to the issue. External participants can only view the issue if the project is public
and issue is not confidential (Service Desk issues are confidential by default).
- `%{ISSUE_DESCRIPTION}`: Issue description at the time email is generated.
If a user has edited the description, it might contain sensitive information that is not intended
to be delivered to external participants. Use this placeholder only if you never modify
descriptions or your team is aware of the template design.
- `%{NOTE_TEXT}`: note text
- `%{UNSUBSCRIBE_URL}`: unsubscribe URL
- `%{SYSTEM_HEADER}`: [system header message](../admin_area/appearance.md#system-header-and-footer-messages)
- `%{SYSTEM_FOOTER}`: [system footer message](../admin_area/appearance.md#system-header-and-footer-messages)
- `%{ADDITIONAL_TEXT}`: [custom additional text](../admin_area/settings/email.md#custom-additional-text)
- `%{NOTE_TEXT}`: Note text.
- `%{UNSUBSCRIBE_URL}`: Unsubscribe URL.
- `%{SYSTEM_HEADER}`: [System header message](../admin_area/appearance.md#system-header-and-footer-messages).
- `%{SYSTEM_FOOTER}`: [System footer message](../admin_area/appearance.md#system-header-and-footer-messages).
- `%{ADDITIONAL_TEXT}`: [Custom additional text](../admin_area/settings/email.md#custom-additional-text).
### Use a custom template for Service Desk issues

View File

@ -32,7 +32,7 @@ module Gitlab
# End date of the range
#
# **period**
# Specifies the period in wich the dates should be generated. Options:
# Specifies the period in which the dates should be generated. Options:
#
# - :day, generate date-value pair for each day in the given period
# - :week, generate date-value pair for each week (beginning of the week date)

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
# This class removes invalid `protected_environment_deploy_access_levels.group_id` records.
class RemoveInvalidDeployAccessLevelGroups < BatchedMigrationJob
operation_name :remove_invalid_deploy_access_level_groups
feature_category :database
scope_to ->(relation) do
relation.joins('INNER JOIN namespaces ON namespaces.id = protected_environment_deploy_access_levels.group_id')
.where.not(protected_environment_deploy_access_levels: { group_id: nil })
.where("namespaces.type = 'User'")
end
def perform
each_sub_batch(&:delete_all)
end
end
end
end

View File

@ -442,10 +442,9 @@ module Gitlab
end
def uid(rep_object)
# We want this explicit to only be username on the FF
# Otherwise, match email.
# There should be no default fall-through on username. Fall-through to import user
if Feature.enabled?(:bitbucket_server_user_mapping_by_username)
# We want this to only match either username or email depending on the flag state.
# There should be no fall-through.
if Feature.enabled?(:bitbucket_server_user_mapping_by_username, type: :ops)
find_user_id(by: :username, value: rep_object.author_username)
else
find_user_id(by: :email, value: rep_object.author_email)

View File

@ -24,7 +24,7 @@ module Gitlab
def uid(object)
# We want this to only match either username or email depending on the flag state.
# There should be no fall-through.
if Feature.enabled?(:bitbucket_server_user_mapping_by_username)
if Feature.enabled?(:bitbucket_server_user_mapping_by_username, type: :ops)
find_user_id(by: :username, value: object.is_a?(Hash) ? object[:author_username] : object.author_username)
else
find_user_id(by: :email, value: object.is_a?(Hash) ? object[:author_email] : object.author_email)

View File

@ -75,8 +75,7 @@ module Gitlab
# We do not want to risk cycles of feature code calling redis calling feature code.
# Also, we only want to benchmark redis-cache, hence repository-cache and rate-limiting are excluded.
!is_a?(Gitlab::Redis::FeatureFlag::FeatureFlagStore) &&
!is_a?(Gitlab::Redis::RepositoryCache::RepositoryCacheStore) &&
!is_a?(Gitlab::Redis::RateLimiting::RateLimitingStore)
!is_a?(Gitlab::Redis::RepositoryCache::RepositoryCacheStore)
end
end
end

View File

@ -3,18 +3,11 @@
module Gitlab
module Redis
class RateLimiting < ::Gitlab::Redis::Wrapper
# We create a subclass only for the purpose of differentiating between different stores in cache metrics
RateLimitingStore = Class.new(ActiveSupport::Cache::RedisCacheStore)
class << self
# The data we store on RateLimiting used to be stored on Cache.
def config_fallback
Cache
end
def cache_store
@cache_store ||= RateLimitingStore.new(redis: pool, namespace: Cache::CACHE_NAMESPACE)
end
end
end
end

View File

@ -42725,6 +42725,9 @@ msgstr ""
msgid "Someone edited this merge request at the same time you did. Please refresh the page to see changes."
msgstr ""
msgid "Someone edited this milestone at the same time you did. Please refresh the page to see changes."
msgstr ""
msgid "Someone edited this test case at the same time you did. The description has been updated and you will need to make your changes again."
msgstr ""

View File

@ -228,7 +228,7 @@
"devDependencies": {
"@gitlab/eslint-plugin": "19.0.0",
"@gitlab/stylelint-config": "4.1.0",
"@graphql-eslint/eslint-plugin": "3.18.0",
"@graphql-eslint/eslint-plugin": "3.19.0",
"@testing-library/dom": "^7.16.2",
"@types/jest": "^28.1.3",
"@vue/compat": "^3.2.47",
@ -245,7 +245,7 @@
"cheerio": "^1.0.0-rc.9",
"commander": "^2.20.3",
"custom-jquery-matchers": "^2.1.0",
"eslint": "8.40.0",
"eslint": "8.41.0",
"eslint-import-resolver-jest": "3.0.2",
"eslint-import-resolver-webpack": "0.13.2",
"eslint-plugin-import": "^2.27.5",

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Groups::MilestonesController do
RSpec.describe Groups::MilestonesController, feature_category: :team_planning do
let(:group) { create(:group, :public) }
let!(:project) { create(:project, :public, group: group) }
let!(:project2) { create(:project, group: group) }
@ -275,6 +275,57 @@ RSpec.describe Groups::MilestonesController do
expect(response).not_to redirect_to(group_milestone_path(group, milestone.iid))
expect(response).to render_template(:edit)
end
context 'with format :json' do
subject do
patch :update,
params: {
id: milestone.iid,
milestone: milestone_params,
group_id: group.to_param,
format: :json
}
end
it "responds :no_content (204) without content body and updates milestone sucessfully" do
subject
expect(response).to have_gitlab_http_status(:no_content)
expect(response.body).to be_blank
milestone.reload
expect(milestone).to have_attributes(title: milestone_params[:title])
end
it 'responds unprocessable_entity (422) with error data' do
# Note: This assignment ensures and triggers a validation error when updating the milestone.
# Same approach used in spec/models/milestone_spec.rb .
milestone_params[:title] = '<img src=x onerror=prompt(1)>'
subject
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response).to include("errors" => be_an(Array))
end
it "handles ActiveRecord::StaleObjectError" do
milestone_params[:title] = "title changed"
# Purposely reduce the `lock_version` to trigger an ActiveRecord::StaleObjectError
milestone_params[:lock_version] = milestone.lock_version - 1
subject
expect(response).to have_gitlab_http_status(:conflict)
expect(json_response).to include "errors" => [
format(
_("Someone edited this %{model_name} at the same time you did. Please refresh your browser and make sure your changes will not unintentionally remove theirs."), # rubocop:disable Layout/LineLength
model_name: _('milestone')
)
]
end
end
end
describe "#destroy" do

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Projects::MilestonesController do
RSpec.describe Projects::MilestonesController, feature_category: :team_planning do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:milestone) { create(:milestone, project: project) }
@ -161,20 +161,92 @@ RSpec.describe Projects::MilestonesController do
{ title: "title changed" }
end
subject do
patch :update,
params: {
id: milestone.iid,
milestone: milestone_params,
namespace_id: project.namespace.id,
project_id: project.id
}
end
# TODO: We should also add more tests for update
it "redirects project milestone show path" do
subject
expect(response).to redirect_to project_milestone_path(project, milestone.iid)
end
it "updates project milestone be_successfully" do
subject
milestone.reload
expect(milestone.title).to eq milestone_params[:title]
end
it "handles ActiveRecord::StaleObjectError" do
# Purposely reduce the lock_version to trigger an ActiveRecord::StaleObjectError
milestone_params[:lock_version] = milestone.lock_version - 1
put :update, params: {
id: milestone.iid,
milestone: milestone_params,
namespace_id: project.namespace.id,
project_id: project.id
}
subject
expect(response).not_to redirect_to(project_milestone_path(project, milestone.iid))
expect(response).to render_template(:edit)
end
context 'with format :json' do
subject do
patch :update,
params: {
id: milestone.iid,
milestone: milestone_params,
namespace_id: project.namespace.id,
project_id: project.id,
format: :json
}
end
it "responds :no_content (204) without content body and updates milestone sucessfully" do
subject
expect(response).to have_gitlab_http_status(:no_content)
expect(response.body).to be_blank
milestone.reload
expect(milestone).to have_attributes(title: milestone_params[:title])
end
it 'responds unprocessable_entity (422) with error data' do
# Note: This assignment ensures and triggers a validation error when updating the milestone.
# Same approach used in spec/models/milestone_spec.rb .
milestone_params[:title] = '<img src=x onerror=prompt(1)>'
subject
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response).to include("errors" => be_an(Array))
end
it "handles ActiveRecord::StaleObjectError" do
milestone_params[:title] = "title changed"
# Purposely reduce the `lock_version` to trigger an ActiveRecord::StaleObjectError
milestone_params[:lock_version] = milestone.lock_version - 1
subject
expect(response).to have_gitlab_http_status(:conflict)
expect(json_response).to include "errors" => [
format(
_("Someone edited this %{model_name} at the same time you did. Please refresh your browser and make sure your changes will not unintentionally remove theirs."), # rubocop:disable Layout/LineLength
model_name: _('milestone')
)
]
end
end
end
describe "#destroy" do

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Group milestone', :js, feature_category: :team_planning do
let_it_be(:group) { create(:group, owner: user) }
let_it_be(:user) { create(:group_member, :maintainer, user: create(:user), group: group).user }
let(:milestone) { create(:milestone, group: group) }
before do
sign_in(user)
end
it_behaves_like 'milestone with interactive markdown task list items in description' do
let(:milestone_path) { group_milestone_path(group, milestone) }
end
end

View File

@ -529,13 +529,13 @@ RSpec.describe 'User edit profile', feature_category: :user_profile do
end
it 'allows the user to select a time zone from a dropdown list of options' do
expect(page.find('.user-time-preferences .dropdown')).not_to have_css('.show')
expect(page).not_to have_selector('.user-time-preferences [data-testid="base-dropdown-menu"]')
page.find('.user-time-preferences .dropdown').click
page.find('.user-time-preferences .gl-new-dropdown-toggle').click
expect(page.find('.user-time-preferences .dropdown')).to have_css('.show')
expect(page.find('.user-time-preferences [data-testid="base-dropdown-menu"]')).to be_visible
page.find("button", text: "Arizona").click
page.find("li", text: "Arizona").click
expect(page).to have_field(:user_timezone, with: 'America/Phoenix', type: :hidden)
end

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Project milestone', :js, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, namespace: user.namespace) }
let(:milestone) { create(:milestone, project: project) }
before do
sign_in(user)
end
it_behaves_like 'milestone with interactive markdown task list items in description' do
let(:milestone_path) { project_milestone_path(project, milestone) }
end
end

View File

@ -413,8 +413,8 @@ RSpec.describe 'Pipeline Schedules', :js, feature_category: :projects do
end
def select_timezone
find('[data-testid="schedule-timezone"] .dropdown-toggle').click
find("button", text: "Arizona").click
find('[data-testid="schedule-timezone"] .gl-new-dropdown-toggle').click
find("li", text: "Arizona").click
end
def select_target_branch

View File

@ -711,27 +711,19 @@ describe('diffs/components/app', () => {
});
it.each`
currentDiffFileId | targetFile | newFileByFile
${'123'} | ${2} | ${false}
${'312'} | ${1} | ${true}
currentDiffFileId | targetFile
${'123'} | ${2}
${'312'} | ${1}
`(
'calls navigateToDiffFileIndex with $index when $link is clicked',
async ({ currentDiffFileId, targetFile, newFileByFile }) => {
createComponent(
{ fileByFileUserPreference: true },
({ state }) => {
state.diffs.treeEntries = {
123: { type: 'blob', fileHash: '123', filePaths: { old: '1234', new: '123' } },
312: { type: 'blob', fileHash: '312', filePaths: { old: '3124', new: '312' } },
};
state.diffs.currentDiffFileId = currentDiffFileId;
},
{
glFeatures: {
singleFileFileByFile: newFileByFile,
},
},
);
async ({ currentDiffFileId, targetFile }) => {
createComponent({ fileByFileUserPreference: true }, ({ state }) => {
state.diffs.treeEntries = {
123: { type: 'blob', fileHash: '123', filePaths: { old: '1234', new: '123' } },
312: { type: 'blob', fileHash: '312', filePaths: { old: '3124', new: '312' } },
};
state.diffs.currentDiffFileId = currentDiffFileId;
});
await nextTick();
@ -741,10 +733,7 @@ describe('diffs/components/app', () => {
await nextTick();
expect(wrapper.vm.navigateToDiffFileIndex).toHaveBeenCalledWith({
index: targetFile - 1,
singleFile: newFileByFile,
});
expect(wrapper.vm.navigateToDiffFileIndex).toHaveBeenCalledWith(targetFile - 1);
},
);
});

View File

@ -1117,67 +1117,50 @@ describe('DiffsStoreActions', () => {
});
describe('when the app is in fileByFile mode', () => {
describe('when the singleFileFileByFile feature flag is enabled', () => {
it('commits SET_CURRENT_DIFF_FILE', () => {
diffActions.goToFile(
{ state, commit, dispatch, getters },
{ path: file.path, singleFile: true },
);
it('commits SET_CURRENT_DIFF_FILE', () => {
diffActions.goToFile({ state, commit, dispatch, getters }, file);
expect(commit).toHaveBeenCalledWith(types.SET_CURRENT_DIFF_FILE, fileHash);
expect(commit).toHaveBeenCalledWith(types.SET_CURRENT_DIFF_FILE, fileHash);
});
it('does nothing more if the path has already been loaded', () => {
getters.isTreePathLoaded = () => true;
diffActions.goToFile({ state, dispatch, getters, commit }, file);
expect(commit).toHaveBeenCalledWith(types.SET_CURRENT_DIFF_FILE, fileHash);
expect(dispatch).toHaveBeenCalledTimes(0);
});
describe('when the tree entry has not been loaded', () => {
it('updates location hash', () => {
diffActions.goToFile({ state, commit, getters, dispatch }, file);
expect(document.location.hash).toBe('#test');
});
it('does nothing more if the path has already been loaded', () => {
getters.isTreePathLoaded = () => true;
it('loads the file and then scrolls to it', async () => {
diffActions.goToFile({ state, commit, getters, dispatch }, file);
diffActions.goToFile(
{ state, dispatch, getters, commit },
{ path: file.path, singleFile: true },
);
// Wait for the fetchFileByFile dispatch to return, to trigger scrollToFile
await waitForPromises();
expect(commit).toHaveBeenCalledWith(types.SET_CURRENT_DIFF_FILE, fileHash);
expect(dispatch).toHaveBeenCalledTimes(0);
expect(dispatch).toHaveBeenCalledWith('fetchFileByFile');
expect(dispatch).toHaveBeenCalledWith('scrollToFile', file);
expect(dispatch).toHaveBeenCalledTimes(2);
});
describe('when the tree entry has not been loaded', () => {
it('updates location hash', () => {
diffActions.goToFile(
{ state, commit, getters, dispatch },
{ path: file.path, singleFile: true },
);
it('shows an alert when there was an error fetching the file', async () => {
dispatch = jest.fn().mockRejectedValue();
expect(document.location.hash).toBe('#test');
});
diffActions.goToFile({ state, commit, getters, dispatch }, file);
it('loads the file and then scrolls to it', async () => {
diffActions.goToFile(
{ state, commit, getters, dispatch },
{ path: file.path, singleFile: true },
);
// Wait for the fetchFileByFile dispatch to return, to trigger the catch
await waitForPromises();
// Wait for the fetchFileByFile dispatch to return, to trigger scrollToFile
await waitForPromises();
expect(dispatch).toHaveBeenCalledWith('fetchFileByFile');
expect(dispatch).toHaveBeenCalledWith('scrollToFile', file);
expect(dispatch).toHaveBeenCalledTimes(2);
});
it('shows an alert when there was an error fetching the file', async () => {
dispatch = jest.fn().mockRejectedValue();
diffActions.goToFile(
{ state, commit, getters, dispatch },
{ path: file.path, singleFile: true },
);
// Wait for the fetchFileByFile dispatch to return, to trigger the catch
await waitForPromises();
expect(createAlert).toHaveBeenCalledTimes(1);
expect(createAlert).toHaveBeenCalledWith({
message: expect.stringMatching(LOAD_SINGLE_DIFF_FAILED),
});
expect(createAlert).toHaveBeenCalledTimes(1);
expect(createAlert).toHaveBeenCalledWith({
message: expect.stringMatching(LOAD_SINGLE_DIFF_FAILED),
});
});
});
@ -1798,17 +1781,17 @@ describe('DiffsStoreActions', () => {
it('commits SET_CURRENT_DIFF_FILE', () => {
return testAction(
diffActions.navigateToDiffFileIndex,
{ index: 0, singleFile: false },
0,
{ flatBlobsList: [{ fileHash: '123' }] },
[{ type: types.SET_CURRENT_DIFF_FILE, payload: '123' }],
[],
);
});
it('dispatches the fetchFileByFile action when the state value viewDiffsFileByFile is true and the single-file file-by-file feature flag is enabled', () => {
it('dispatches the fetchFileByFile action when the state value viewDiffsFileByFile is true', () => {
return testAction(
diffActions.navigateToDiffFileIndex,
{ index: 0, singleFile: true },
0,
{ viewDiffsFileByFile: true, flatBlobsList: [{ fileHash: '123' }] },
[{ type: types.SET_CURRENT_DIFF_FILE, payload: '123' }],
[{ type: 'fetchFileByFile' }],

View File

@ -1,4 +1,4 @@
import { GlDropdownItem, GlDropdown, GlSearchBoxByType } from '@gitlab/ui';
import { GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown/timezone_dropdown.vue';
@ -9,7 +9,8 @@ describe('Deploy freeze timezone dropdown', () => {
let wrapper;
let store;
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
const findSearchBox = () => wrapper.findByTestId('listbox-search-input');
const createComponent = async (searchTerm, selectedTimezone) => {
wrapper = shallowMountExtended(TimezoneDropdown, {
@ -19,15 +20,18 @@ describe('Deploy freeze timezone dropdown', () => {
timezoneData: timezoneDataFixture,
name: 'user[timezone]',
},
stubs: {
GlCollapsibleListbox,
},
});
findSearchBox().vm.$emit('input', searchTerm);
await nextTick();
};
const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findDropdownItemByIndex = (index) => wrapper.findAllComponents(GlDropdownItem).at(index);
const findEmptyResultsItem = () => wrapper.findByTestId('noMatchingResults');
const findAllDropdownItems = () => wrapper.findAllComponents(GlListboxItem);
const findDropdownItemByIndex = (index) => findAllDropdownItems().at(index);
const findEmptyResultsItem = () => wrapper.findByTestId('listbox-no-results-text');
const findHiddenInput = () => wrapper.find('input');
describe('No time zones found', () => {
@ -36,7 +40,8 @@ describe('Deploy freeze timezone dropdown', () => {
});
it('renders empty results message', () => {
expect(findDropdownItemByIndex(0).text()).toBe('No matching results');
expect(findEmptyResultsItem().exists()).toBe(true);
expect(findEmptyResultsItem().text()).toBe('No matching results');
});
});
@ -69,11 +74,13 @@ describe('Deploy freeze timezone dropdown', () => {
const selectedTz = findTzByName('Alaska');
it('should emit input if a time zone is clicked', () => {
findDropdownItemByIndex(0).vm.$emit('click');
const payload = formatTimezone(selectedTz);
findDropdown().vm.$emit('select', payload);
expect(wrapper.emitted('input')).toEqual([
[
{
formattedTimezone: formatTimezone(selectedTz),
formattedTimezone: payload,
identifier: selectedTz.identifier,
},
],
@ -88,7 +95,7 @@ describe('Deploy freeze timezone dropdown', () => {
});
it('renders empty selections', () => {
expect(wrapper.findComponent(GlDropdown).props().text).toBe('Select timezone');
expect(findDropdown().props('toggleText')).toBe('Select timezone');
});
it('preserves initial value in the associated input', () => {
@ -102,14 +109,14 @@ describe('Deploy freeze timezone dropdown', () => {
});
it('renders selected time zone as dropdown label', () => {
expect(wrapper.findComponent(GlDropdown).props().text).toBe('[UTC+2] Berlin');
expect(findDropdown().props('toggleText')).toBe('[UTC+2] Berlin');
});
it('adds a checkmark to the selected option', async () => {
const selectedTZOption = findAllDropdownItems().at(0);
selectedTZOption.vm.$emit('click');
findDropdown().vm.$emit('select', formatTimezone(findTzByName('Abu Dhabi')));
await nextTick();
expect(selectedTZOption.attributes('ischecked')).toBe('true');
expect(findDropdownItemByIndex(0).props('isSelected')).toBe(true);
});
});
});

View File

@ -27,15 +27,8 @@ RSpec.describe SafeFormatHelper, feature_category: :shared do
result: '<b>strong</b> &lt;a href=&quot;&quot;&gt;link&lt;/a&gt;'
context 'when format is marked as html_safe' do
let(:format) { '<b>strong</b>'.html_safe }
let(:args) { {} }
it 'raises an error' do
message = 'Argument `format` must not be marked as html_safe!'
expect { helper.safe_format(format, **args) }
.to raise_error ArgumentError, message
end
it_behaves_like 'safe formatting', '<b>strong</b>'.html_safe, args: {},
result: '&lt;b&gt;strong&lt;/b&gt;'
end
context 'with a view component' do
@ -54,5 +47,19 @@ RSpec.describe SafeFormatHelper, feature_category: :shared do
.to eq('&lt;b&gt;&lt;br&gt;&lt;/b&gt;')
end
end
context 'with format containing escaped entities' do
it_behaves_like 'safe formatting', 'In &lt; hour',
args: {},
result: 'In &lt; hour'
it_behaves_like 'safe formatting', '&quot;air&quot;',
args: {},
result: '&quot;air&quot;'
it_behaves_like 'safe formatting', 'Mix & match &gt; all',
args: {},
result: 'Mix &amp; match &gt; all'
end
end
end

View File

@ -0,0 +1,57 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::RemoveInvalidDeployAccessLevelGroups,
:migration, schema: 20230519011151, feature_category: :continuous_delivery do
let!(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
let!(:project) { table(:projects).create!(namespace_id: namespace.id, project_namespace_id: namespace.id) }
let!(:group) { table(:namespaces).create!(name: 'group', path: 'group', type: 'Group') }
let!(:user) { table(:users).create!(email: 'deployer@example.com', username: 'deployer', projects_limit: 0) }
let!(:protected_environment) { table(:protected_environments).create!(project_id: project.id, name: 'production') }
let(:migration) do
described_class.new(
start_id: 1, end_id: 1000,
batch_table: :protected_environment_deploy_access_levels, batch_column: :id,
sub_batch_size: 10, pause_ms: 0,
connection: ApplicationRecord.connection
)
end
describe '#perform' do
let!(:deploy_access_level_access_level) do
table(:protected_environment_deploy_access_levels)
.create!(protected_environment_id: protected_environment.id, access_level: 40)
end
let!(:deploy_access_level_user) do
table(:protected_environment_deploy_access_levels)
.create!(protected_environment_id: protected_environment.id, user_id: user.id)
end
let!(:deploy_access_level_group) do
table(:protected_environment_deploy_access_levels)
.create!(protected_environment_id: protected_environment.id, group_id: group.id)
end
let!(:deploy_access_level_namespace) do
table(:protected_environment_deploy_access_levels)
.create!(protected_environment_id: protected_environment.id, group_id: namespace.id)
end
it 'backfill tiers for all environments in range' do
expect(deploy_access_level_access_level).to be_present
expect(deploy_access_level_user).to be_present
expect(deploy_access_level_group).to be_present
expect(deploy_access_level_namespace).to be_present
migration.perform
expect { deploy_access_level_access_level.reload }.not_to raise_error
expect { deploy_access_level_user.reload }.not_to raise_error
expect { deploy_access_level_group.reload }.not_to raise_error
expect { deploy_access_level_namespace.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end

View File

@ -77,7 +77,6 @@ RSpec.describe Gitlab::Patch::RedisCacheStore, :use_clean_rails_redis_caching, f
context 'when reading from non redis-cache stores' do
it_behaves_like 'reading using non redis cache stores', Gitlab::Redis::RepositoryCache
it_behaves_like 'reading using non redis cache stores', Gitlab::Redis::FeatureFlag
it_behaves_like 'reading using non redis cache stores', Gitlab::Redis::RateLimiting
end
context 'when feature flag is disabled' do
@ -133,7 +132,6 @@ RSpec.describe Gitlab::Patch::RedisCacheStore, :use_clean_rails_redis_caching, f
context 'when deleting from non redis-cache stores' do
it_behaves_like 'deleting using non redis cache stores', Gitlab::Redis::RepositoryCache
it_behaves_like 'deleting using non redis cache stores', Gitlab::Redis::FeatureFlag
it_behaves_like 'deleting using non redis cache stores', Gitlab::Redis::RateLimiting
end
context 'when deleting large amount of keys' do

View File

@ -4,10 +4,4 @@ require 'spec_helper'
RSpec.describe Gitlab::Redis::RateLimiting do
include_examples "redis_new_instance_shared_examples", 'rate_limiting', Gitlab::Redis::Cache
describe '.cache_store' do
it 'uses the CACHE_NAMESPACE namespace' do
expect(described_class.cache_store.options[:namespace]).to eq(Gitlab::Redis::Cache::CACHE_NAMESPACE)
end
end
end

View File

@ -211,6 +211,28 @@ RSpec.describe Emails::ServiceDesk, feature_category: :service_desk do
it_behaves_like 'a service desk notification email with template content', 'thank_you'
end
context 'when issue url placeholder is used' do
let(:full_issue_url) { issue_url(issue) }
let(:template_content) { 'thank you, your new issue has been created. %{ISSUE_URL}' }
let(:expected_template_html) do
"<p dir=\"auto\">thank you, your new issue has been created. " \
"<a href=\"#{full_issue_url}\">#{full_issue_url}</a></p>"
end
it_behaves_like 'a service desk notification email with template content', 'thank_you'
context 'when it is used in markdown format' do
let(:template_content) { 'thank you, your new issue has been created. [%{ISSUE_PATH}](%{ISSUE_URL})' }
let(:issue_path) { "#{project.full_path}##{issue.iid}" }
let(:expected_template_html) do
"<p dir=\"auto\">thank you, your new issue has been created. " \
"<a href=\"#{full_issue_url}\">#{issue_path}</a></p>"
end
it_behaves_like 'a service desk notification email with template content', 'thank_you'
end
end
end
end

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe ScheduleToRemoveInvalidDeployAccessLevelGroups, feature_category: :continuous_delivery do
let!(:batched_migration) { described_class::MIGRATION }
it 'schedules a new batched migration' do
reversible_migration do |migration|
migration.before -> {
expect(batched_migration).not_to have_scheduled_batched_migration
}
migration.after -> {
expect(batched_migration).to have_scheduled_batched_migration(
table_name: :protected_environment_deploy_access_levels,
column_name: :id,
interval: described_class::DELAY_INTERVAL
)
}
end
end
end

View File

@ -102,13 +102,13 @@ RSpec.describe CustomerRelations::Organization, type: :model do
)
end
subject(:found_organizations) { group.organizations.search(search_term) }
subject(:found_crm_organizations) { group.crm_organizations.search(search_term) }
context 'when search term is empty' do
let(:search_term) { "" }
it 'returns all group organizations' do
expect(found_organizations).to contain_exactly(crm_organization_a, crm_organization_b)
it 'returns all group crm_organizations' do
expect(found_crm_organizations).to contain_exactly(crm_organization_a, crm_organization_b)
end
end
@ -137,13 +137,13 @@ RSpec.describe CustomerRelations::Organization, type: :model do
let_it_be(:crm_organization_a) { create(:crm_organization, group: group, state: "inactive") }
let_it_be(:crm_organization_b) { create(:crm_organization, group: group, state: "active") }
context 'when searching for organizations state' do
it 'returns only inactive organizations' do
expect(group.organizations.search_by_state(:inactive)).to contain_exactly(crm_organization_a)
context 'when searching for crm_organizations state' do
it 'returns only inactive crm_organizations' do
expect(group.crm_organizations.search_by_state(:inactive)).to contain_exactly(crm_organization_a)
end
it 'returns only active organizations' do
expect(group.organizations.search_by_state(:active)).to contain_exactly(crm_organization_b)
it 'returns only active crm_organizations' do
expect(group.crm_organizations.search_by_state(:active)).to contain_exactly(crm_organization_b)
end
end
end
@ -154,15 +154,15 @@ RSpec.describe CustomerRelations::Organization, type: :model do
create_list(:crm_organization, 2, group: group, state: 'inactive')
end
it 'returns correct organization counts' do
counts = group.organizations.counts_by_state
it 'returns correct crm_organization counts' do
counts = group.crm_organizations.counts_by_state
expect(counts['active']).to be(3)
expect(counts['inactive']).to be(2)
end
it 'returns 0 with no results' do
counts = group.organizations.where(id: non_existing_record_id).counts_by_state
counts = group.crm_organizations.where(id: non_existing_record_id).counts_by_state
expect(counts['active']).to be(0)
expect(counts['inactive']).to be(0)
@ -176,13 +176,13 @@ RSpec.describe CustomerRelations::Organization, type: :model do
describe '.sort_by_name' do
it 'sorts them by name in ascendent order' do
expect(group.organizations.sort_by_name).to eq([crm_organization_b, crm_organization_c, crm_organization_a])
expect(group.crm_organizations.sort_by_name).to eq([crm_organization_b, crm_organization_c, crm_organization_a])
end
end
describe '.sort_by_field' do
it 'sorts them by description in descending order' do
expect(group.organizations.sort_by_field('description', :desc))
expect(group.crm_organizations.sort_by_field('description', :desc))
.to eq([crm_organization_c, crm_organization_a, crm_organization_b])
end
end

View File

@ -49,7 +49,7 @@ RSpec.describe Group, feature_category: :subgroups do
end
it { is_expected.to have_many(:contacts).class_name('CustomerRelations::Contact') }
it { is_expected.to have_many(:organizations).class_name('CustomerRelations::Organization') }
it { is_expected.to have_many(:crm_organizations).class_name('CustomerRelations::Organization') }
it { is_expected.to have_many(:protected_branches).inverse_of(:group).with_foreign_key(:namespace_id) }
it { is_expected.to have_one(:crm_settings) }
it { is_expected.to have_one(:group_feature) }
@ -3176,13 +3176,13 @@ RSpec.describe Group, feature_category: :subgroups do
end
end
describe '.organizations' do
it 'returns organizations belonging to the group' do
describe '.crm_organizations' do
it 'returns crm_organizations belonging to the group' do
crm_organization1 = create(:crm_organization, group: group)
create(:crm_organization)
crm_organization3 = create(:crm_organization, group: group)
expect(group.organizations).to contain_exactly(crm_organization1, crm_organization3)
expect(group.crm_organizations).to contain_exactly(crm_organization1, crm_organization3)
end
end

View File

@ -907,7 +907,7 @@ RSpec.describe Groups::TransferService, :sidekiq_inline, feature_category: :subg
let(:subsub_project) { create(:project, group: subsubgroup) }
let!(:contacts) { create_list(:contact, 4, group: root_group) }
let!(:organizations) { create_list(:crm_organization, 2, group: root_group) }
let!(:crm_organizations) { create_list(:crm_organization, 2, group: root_group) }
before do
create(:issue_customer_relations_contact, contact: contacts[0], issue: create(:issue, project: root_project))
@ -966,7 +966,7 @@ RSpec.describe Groups::TransferService, :sidekiq_inline, feature_category: :subg
it 'moves all crm objects' do
expect { transfer_service.execute(new_parent_group) }
.to change { root_group.contacts.count }.by(-4)
.and change { root_group.organizations.count }.by(-2)
.and change { root_group.crm_organizations.count }.by(-2)
end
it 'retains issue contacts' do
@ -991,7 +991,7 @@ RSpec.describe Groups::TransferService, :sidekiq_inline, feature_category: :subg
it 'moves all crm objects' do
expect { transfer_service.execute(subgroup_in_new_parent_group) }
.to change { root_group.contacts.count }.by(-4)
.and change { root_group.organizations.count }.by(-2)
.and change { root_group.crm_organizations.count }.by(-2)
end
it 'retains issue contacts' do

View File

@ -0,0 +1,54 @@
# frozen_string_literal: true
RSpec.shared_examples 'milestone with interactive markdown task list items in description' do
let(:markdown) do
<<-MARKDOWN.strip_heredoc
This is a task list:
- [ ] Incomplete task list item 1
- [x] Complete task list item 1
- [ ] Incomplete task list item 2
- [x] Complete task list item 2
- [ ] Incomplete task list item 3
- [ ] Incomplete task list item 4
MARKDOWN
end
before do
milestone.update!(description: markdown)
end
it 'renders task list in description' do
visit milestone_path
wait_for_requests
within('ul.task-list') do
expect(page).to have_selector('li.task-list-item', count: 6)
expect(page).to have_selector('li.task-list-item input.task-list-item-checkbox[checked]', count: 2)
end
end
it 'allows interaction with task list item checkboxes' do
visit milestone_path
wait_for_requests
within('ul.task-list') do
within('li.task-list-item', text: 'Incomplete task list item 1') do
find('input.task-list-item-checkbox').click
wait_for_requests
end
expect(page).to have_selector('li.task-list-item', count: 6)
page.all('li.task-list-item input.task-list-item-checkbox') { |element| expect(element).to be_checked }
# After page reload, the task list items should still be checked
visit milestone_path
wait_for_requests
expect(page).to have_selector('ul input[type="checkbox"][checked]', count: 3)
end
end
end

View File

@ -1059,10 +1059,10 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
"@eslint/js@8.40.0":
version "8.40.0"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.40.0.tgz#3ba73359e11f5a7bd3e407f70b3528abfae69cec"
integrity sha512-ElyB54bJIhXQYVKjDSvCkPO1iU1tSAeVQJbllWJq1XQSmmA4dgFk8CbiBGpiOPxleE48vDogxCtmMYku4HSVLA==
"@eslint/js@8.41.0":
version "8.41.0"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.41.0.tgz#080321c3b68253522f7646b55b577dd99d2950b3"
integrity sha512-LxcyMGxwmTh2lY9FwHPGWOHmYFCZvbrFCBZL4FzSSsxsRPuhrYUg/49/0KDfW8tnIEaEHtfmn6+NPN+1DqaNmA==
"@gitlab/at.js@1.5.7":
version "1.5.7"
@ -1139,10 +1139,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/web-ide/-/web-ide-0.0.1-dev-20230511143809.tgz#c13dfb4d1edab2e020d4a102d4ec18048917490f"
integrity sha512-caP5WSaTuIhPrPGUWyvPT4np6swkKQHM1Pa9HiBnGhiOhhQ1+3X/+J9EoZXUhnhwiBzS7sp32Uyttam4am/sTA==
"@graphql-eslint/eslint-plugin@3.18.0":
version "3.18.0"
resolved "https://registry.yarnpkg.com/@graphql-eslint/eslint-plugin/-/eslint-plugin-3.18.0.tgz#071b5580d1d47ac0f25fd4296fea4105ddd8e401"
integrity sha512-riEEfRycc0+pWxcEWqHi8woRxzg1xZqAfh9DRACJUR7bTN8dmc1N04i7+pvW4sevClUFYC2wuL1Vtr+DwzXLUg==
"@graphql-eslint/eslint-plugin@3.19.0":
version "3.19.0"
resolved "https://registry.yarnpkg.com/@graphql-eslint/eslint-plugin/-/eslint-plugin-3.19.0.tgz#08cf96f7b093622449064bc258526a73f92944eb"
integrity sha512-p1jK3IUTi+wecMAzeWpDWQE3ZskayKvE6sFnELaVqmYERJhsocKp1yoVWgWfLuSDgtcMEKG7YHz8OQCmy/9Siw==
dependencies:
"@babel/code-frame" "^7.18.6"
"@graphql-tools/code-file-loader" "^7.3.6"
@ -5819,15 +5819,15 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994"
integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==
eslint@8.40.0:
version "8.40.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.40.0.tgz#a564cd0099f38542c4e9a2f630fa45bf33bc42a4"
integrity sha512-bvR+TsP9EHL3TqNtj9sCNJVAFK3fBN8Q7g5waghxyRsPLIMwL73XSKnZFK0hk/O2ANC+iAoq6PWMQ+IfBAJIiQ==
eslint@8.41.0:
version "8.41.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.41.0.tgz#3062ca73363b4714b16dbc1e60f035e6134b6f1c"
integrity sha512-WQDQpzGBOP5IrXPo4Hc0814r4/v2rrIsB0rhT7jtunIalgg6gYXWhRMOejVO8yH21T/FGaxjmFjBMNqcIlmH1Q==
dependencies:
"@eslint-community/eslint-utils" "^4.2.0"
"@eslint-community/regexpp" "^4.4.0"
"@eslint/eslintrc" "^2.0.3"
"@eslint/js" "8.40.0"
"@eslint/js" "8.41.0"
"@humanwhocodes/config-array" "^0.11.8"
"@humanwhocodes/module-importer" "^1.0.1"
"@nodelib/fs.walk" "^1.2.8"
@ -5847,13 +5847,12 @@ eslint@8.40.0:
find-up "^5.0.0"
glob-parent "^6.0.2"
globals "^13.19.0"
grapheme-splitter "^1.0.4"
graphemer "^1.4.0"
ignore "^5.2.0"
import-fresh "^3.0.0"
imurmurhash "^0.1.4"
is-glob "^4.0.0"
is-path-inside "^3.0.3"
js-sdsl "^4.1.4"
js-yaml "^4.1.0"
json-stable-stringify-without-jsonify "^1.0.1"
levn "^0.4.1"
@ -6620,10 +6619,10 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
grapheme-splitter@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e"
integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==
graphemer@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
graphql-config@^4.4.0:
version "4.5.0"
@ -8053,11 +8052,6 @@ js-cookie@^3.0.0:
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.1.tgz#9e39b4c6c2f56563708d7d31f6f5f21873a92414"
integrity sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw==
js-sdsl@^4.1.4:
version "4.1.4"
resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.1.4.tgz#78793c90f80e8430b7d8dc94515b6c77d98a26a6"
integrity sha512-Y2/yD55y5jteOAmY50JbUZYwk3CP3wnLPEZnlR1w9oKhITrBEtAxwuWKebFf8hMrPMgbYwFoWK/lH2sBkErELw==
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"