diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-23 15:10:24 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-23 15:10:24 +0300 |
commit | e6d048d769240760008f0dbb6b811e1ebc675292 (patch) | |
tree | e005f05e66518044c7e8bc33e263f12f2561708d | |
parent | 611b009e924c25fcc8706b52af90542c4b78f0c0 (diff) |
Add latest changes from gitlab-org/gitlab@master
60 files changed, 723 insertions, 340 deletions
diff --git a/.gitlab/ci/review-apps/main.gitlab-ci.yml b/.gitlab/ci/review-apps/main.gitlab-ci.yml index 680254a6640..8224d9eee5c 100644 --- a/.gitlab/ci/review-apps/main.gitlab-ci.yml +++ b/.gitlab/ci/review-apps/main.gitlab-ci.yml @@ -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} @@ -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' diff --git a/Gemfile.checksum b/Gemfile.checksum index bc127771a51..63e4d83f50c 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -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"}, diff --git a/Gemfile.lock b/Gemfile.lock index 637738b31e9..857f0fe3a03 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index 02307150e2f..f644c69f0a0 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -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() { diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue index 4c2cb83ffb3..64a7c047cb4 100644 --- a/app/assets/javascripts/diffs/components/diff_file.vue +++ b/app/assets/javascripts/diffs/components/diff_file.vue @@ -209,12 +209,6 @@ export default { if (this.hasDiff) { this.postRender(); - } else if ( - this.viewDiffsFileByFile && - !this.isCollapsed && - !this.glFeatures.singleFileFileByFile - ) { - this.requestDiff(); } this.manageViewedEffects(); diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue index 4f1875e9175..544bbbfe9d8 100644 --- a/app/assets/javascripts/diffs/components/tree_list.vue +++ b/app/assets/javascripts/diffs/components/tree_list.vue @@ -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> diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 3be6562f586..30c34e9a5be 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -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'); } }; diff --git a/app/assets/javascripts/issues/constants.js b/app/assets/javascripts/issues/constants.js index c79612ad5d0..444ee704521 100644 --- a/app/assets/javascripts/issues/constants.js +++ b/app/assets/javascripts/issues/constants.js @@ -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'; diff --git a/app/assets/javascripts/milestones/index.js b/app/assets/javascripts/milestones/index.js index 8780d931588..420f7cee4d2 100644 --- a/app/assets/javascripts/milestones/index.js +++ b/app/assets/javascripts/milestones/index.js @@ -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() { diff --git a/app/assets/javascripts/vue_shared/components/timezone_dropdown/timezone_dropdown.vue b/app/assets/javascripts/vue_shared/components/timezone_dropdown/timezone_dropdown.vue index 247f49c1345..04dc3b20dff 100644 --- a/app/assets/javascripts/vue_shared/components/timezone_dropdown/timezone_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/timezone_dropdown/timezone_dropdown.vue @@ -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> diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index 903c8c214ae..5f6b55ea928 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -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 diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 91788444f1f..4423929e48b 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -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) diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index 569a514b23b..35b65dbce7e 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -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| diff --git a/app/finders/crm/organizations_finder.rb b/app/finders/crm/organizations_finder.rb index 69f72235c71..8701d26dd6e 100644 --- a/app/finders/crm/organizations_finder.rb +++ b/app/finders/crm/organizations_finder.rb @@ -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? diff --git a/app/helpers/safe_format_helper.rb b/app/helpers/safe_format_helper.rb index f05cf5ab50f..96124e98d4e 100644 --- a/app/helpers/safe_format_helper.rb +++ b/app/helpers/safe_format_helper.rb @@ -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 <b>bold</b> + # safe_format('In < hour & more') + # # => 'In < hour & 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 diff --git a/app/mailers/emails/service_desk.rb b/app/mailers/emails/service_desk.rb index c627f4633e4..a51a2a68da0 100644 --- a/app/mailers/emails/service_desk.rb +++ b/app/mailers/emails/service_desk.rb @@ -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 diff --git a/app/models/group.rb b/app/models/group.rb index 844b7496913..9ef1d3f12f3 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -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' diff --git a/app/services/groups/transfer_service.rb b/app/services/groups/transfer_service.rb index 1c8df157716..16454360ee2 100644 --- a/app/services/groups/transfer_service.rb +++ b/app/services/groups/transfer_service.rb @@ -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 diff --git a/app/views/projects/merge_requests/_page.html.haml b/app/views/projects/merge_requests/_page.html.haml index 3e56148f777..5ea67376a86 100644 --- a/app/views/projects/merge_requests/_page.html.haml +++ b/app/views/projects/merge_requests/_page.html.haml @@ -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 } } diff --git a/app/views/shared/milestones/_description.html.haml b/app/views/shared/milestones/_description.html.haml index a63702661d0..3774fb0869f 100644 --- a/app/views/shared/milestones/_description.html.haml +++ b/app/views/shared/milestones/_description.html.haml @@ -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)} } diff --git a/config/feature_flags/development/bitbucket_server_user_mapping_by_username.yml b/config/feature_flags/ops/bitbucket_server_user_mapping_by_username.yml index c672eb1e64e..9d86b4f5af4 100644 --- a/config/feature_flags/development/bitbucket_server_user_mapping_by_username.yml +++ b/config/feature_flags/ops/bitbucket_server_user_mapping_by_username.yml @@ -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 diff --git a/config/feature_flags/ops/code_suggestions_tokens_api.yml b/config/feature_flags/ops/code_suggestions_tokens_api.yml new file mode 100644 index 00000000000..9fc2a5358cc --- /dev/null +++ b/config/feature_flags/ops/code_suggestions_tokens_api.yml @@ -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 diff --git a/data/deprecations/15-9-database-single-database-connection-conf.yml b/data/deprecations/15-9-database-single-database-connection-conf.yml index ee66c987032..de4ae51d615 100644 --- a/data/deprecations/15-9-database-single-database-connection-conf.yml +++ b/data/deprecations/15-9-database-single-database-connection-conf.yml @@ -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 diff --git a/data/deprecations/16-1-non-decomposed-mode-deprecation.yml b/data/deprecations/16-1-non-decomposed-mode-deprecation.yml new file mode 100644 index 00000000000..963fc0d8230 --- /dev/null +++ b/data/deprecations/16-1-non-decomposed-mode-deprecation.yml @@ -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. diff --git a/db/docs/batched_background_migrations/remove_invalid_deploy_access_level_groups.yml b/db/docs/batched_background_migrations/remove_invalid_deploy_access_level_groups.yml new file mode 100644 index 00000000000..39d13b58443 --- /dev/null +++ b/db/docs/batched_background_migrations/remove_invalid_deploy_access_level_groups.yml @@ -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 diff --git a/db/post_migrate/20230519011151_schedule_to_remove_invalid_deploy_access_level_groups.rb b/db/post_migrate/20230519011151_schedule_to_remove_invalid_deploy_access_level_groups.rb new file mode 100644 index 00000000000..c3bd64634ce --- /dev/null +++ b/db/post_migrate/20230519011151_schedule_to_remove_invalid_deploy_access_level_groups.rb @@ -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 diff --git a/db/schema_migrations/20230519011151 b/db/schema_migrations/20230519011151 new file mode 100644 index 00000000000..7a9f4dbcee4 --- /dev/null +++ b/db/schema_migrations/20230519011151 @@ -0,0 +1 @@ +c2340753bf27ef119dd76a49ada76f07ef6f22577ae11651e81bba6bd7502f08
\ No newline at end of file diff --git a/doc/administration/geo/replication/geo_validation_tests.md b/doc/administration/geo/replication/geo_validation_tests.md index cad3a396bfc..97c10b3ec0a 100644 --- a/doc/administration/geo/replication/geo_validation_tests.md +++ b/doc/administration/geo/replication/geo_validation_tests.md @@ -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,14 +184,17 @@ The following are PostgreSQL upgrade validation tests we performed. The following are additional validation tests we performed. -### May 2021 +### April 2022 -[Test failover with object storage replication enabled](https://gitlab.com/gitlab-org/gitlab/-/issues/330362): +[Validate Object storage replication using AWS based object storage](https://gitlab.com/gitlab-org/gitlab/-/issues/351463): -- 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) +- Description: Tested the average time it takes for a single image to replicate from the primary object storage location to the secondary when using AWS 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 AWS managed replication the average time for an image to replicate between sites is about 49 seconds, this is true for when sites are located in the same region and when they are further apart (Europe to America). When using Geo managed replication in the same region the average time for replication took just 5 seconds, however when replicating cross region the average time rose to 33 seconds. + +[Validate Object storage replication using GCP based object storage](https://gitlab.com/gitlab-org/gitlab/-/issues/351464): + +- 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 @@ -202,17 +205,14 @@ The following are additional validation tests we performed. - 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): - -- Description: Tested the average time it takes for a single image to replicate from the primary object storage location to the secondary when using AWS 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 AWS managed replication the average time for an image to replicate between sites is about 49 seconds, this is true for when sites are located in the same region and when they are further apart (Europe to America). When using Geo managed replication in the same region the average time for replication took just 5 seconds, however when replicating cross region the average time rose to 33 seconds. +### May 2021 -[Validate Object storage replication using GCP based object storage](https://gitlab.com/gitlab-org/gitlab/-/issues/351464): +[Test failover with object storage replication enabled](https://gitlab.com/gitlab-org/gitlab/-/issues/330362): -- 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. +- 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 diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md index ac14b1b5ea2..0ca943a39c9 100644 --- a/doc/development/i18n/externalization.md +++ b/doc/development/i18n/externalization.md @@ -589,7 +589,7 @@ instead: - In Ruby/HAML: ```ruby - html_escape_once(_('In < 1 hour')).html_safe + safe_format(_('In < 1 hour')) # => 'In < 1 hour' ``` diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md index ee71a8b9a04..8d4b45209c9 100644 --- a/doc/update/deprecations.md +++ b/doc/update/deprecations.md @@ -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"> diff --git a/doc/user/project/service_desk.md b/doc/user/project/service_desk.md index bd5c8214e68..f19ddef8496 100644 --- a/doc/user/project/service_desk.md +++ b/doc/user/project/service_desk.md @@ -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 diff --git a/lib/gitlab/analytics/date_filler.rb b/lib/gitlab/analytics/date_filler.rb index aa3db9f3635..33ebe269f26 100644 --- a/lib/gitlab/analytics/date_filler.rb +++ b/lib/gitlab/analytics/date_filler.rb @@ -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) diff --git a/lib/gitlab/background_migration/remove_invalid_deploy_access_level_groups.rb b/lib/gitlab/background_migration/remove_invalid_deploy_access_level_groups.rb new file mode 100644 index 00000000000..0a107a136b0 --- /dev/null +++ b/lib/gitlab/background_migration/remove_invalid_deploy_access_level_groups.rb @@ -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 diff --git a/lib/gitlab/bitbucket_server_import/importer.rb b/lib/gitlab/bitbucket_server_import/importer.rb index 6b163cd1b2d..f3253027d57 100644 --- a/lib/gitlab/bitbucket_server_import/importer.rb +++ b/lib/gitlab/bitbucket_server_import/importer.rb @@ -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) diff --git a/lib/gitlab/bitbucket_server_import/user_finder.rb b/lib/gitlab/bitbucket_server_import/user_finder.rb index f96454eb2cc..68bd2d4851a 100644 --- a/lib/gitlab/bitbucket_server_import/user_finder.rb +++ b/lib/gitlab/bitbucket_server_import/user_finder.rb @@ -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) diff --git a/lib/gitlab/patch/redis_cache_store.rb b/lib/gitlab/patch/redis_cache_store.rb index f71262896d1..172ecab57fb 100644 --- a/lib/gitlab/patch/redis_cache_store.rb +++ b/lib/gitlab/patch/redis_cache_store.rb @@ -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 diff --git a/lib/gitlab/redis/rate_limiting.rb b/lib/gitlab/redis/rate_limiting.rb index 74b4ca12d18..30ec44b748d 100644 --- a/lib/gitlab/redis/rate_limiting.rb +++ b/lib/gitlab/redis/rate_limiting.rb @@ -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 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index cfb426ef1da..437f090921d 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 "" diff --git a/package.json b/package.json index 3b4a061d752..dcf1a59fec8 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb index 87030448b30..fa2a2277e85 100644 --- a/spec/controllers/groups/milestones_controller_spec.rb +++ b/spec/controllers/groups/milestones_controller_spec.rb @@ -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 diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb index e2b73e55145..f94c14f209d 100644 --- a/spec/controllers/projects/milestones_controller_spec.rb +++ b/spec/controllers/projects/milestones_controller_spec.rb @@ -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 diff --git a/spec/features/groups/milestones/milestone_showing_spec.rb b/spec/features/groups/milestones/milestone_showing_spec.rb new file mode 100644 index 00000000000..ca556cf159c --- /dev/null +++ b/spec/features/groups/milestones/milestone_showing_spec.rb @@ -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 diff --git a/spec/features/profiles/user_edit_profile_spec.rb b/spec/features/profiles/user_edit_profile_spec.rb index a6dcbc31dc4..3ce1c3a33a0 100644 --- a/spec/features/profiles/user_edit_profile_spec.rb +++ b/spec/features/profiles/user_edit_profile_spec.rb @@ -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 diff --git a/spec/features/projects/milestones/milestone_showing_spec.rb b/spec/features/projects/milestones/milestone_showing_spec.rb new file mode 100644 index 00000000000..b68f569221a --- /dev/null +++ b/spec/features/projects/milestones/milestone_showing_spec.rb @@ -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 diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb index 81e003d7d1c..22c7e00be6b 100644 --- a/spec/features/projects/pipeline_schedules_spec.rb +++ b/spec/features/projects/pipeline_schedules_spec.rb @@ -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 diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js index 42eec0af961..29ade6514be 100644 --- a/spec/frontend/diffs/components/app_spec.js +++ b/spec/frontend/diffs/components/app_spec.js @@ -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); }, ); }); diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js index f084458b5c7..d32c2d4665d 100644 --- a/spec/frontend/diffs/store/actions_spec.js +++ b/spec/frontend/diffs/store/actions_spec.js @@ -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; + it('does nothing more if the path has already been loaded', () => { + getters.isTreePathLoaded = () => true; - diffActions.goToFile( - { state, dispatch, getters, commit }, - { path: file.path, singleFile: true }, - ); + diffActions.goToFile({ state, dispatch, getters, commit }, file); - expect(commit).toHaveBeenCalledWith(types.SET_CURRENT_DIFF_FILE, fileHash); - expect(dispatch).toHaveBeenCalledTimes(0); - }); + 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 }, - { path: file.path, singleFile: true }, - ); + 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'); - }); + expect(document.location.hash).toBe('#test'); + }); - it('loads the file and then scrolls to it', async () => { - diffActions.goToFile( - { state, commit, getters, dispatch }, - { path: file.path, singleFile: true }, - ); + it('loads the file and then scrolls to it', async () => { + diffActions.goToFile({ state, commit, getters, dispatch }, file); - // Wait for the fetchFileByFile dispatch to return, to trigger scrollToFile - 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); - }); + 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(); + 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 }, - ); + diffActions.goToFile({ state, commit, getters, dispatch }, file); - // Wait for the fetchFileByFile dispatch to return, to trigger the catch - await waitForPromises(); + // 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' }], diff --git a/spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js b/spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js index d8dedd8240b..ecf6a776a4b 100644 --- a/spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js +++ b/spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js @@ -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); }); }); }); diff --git a/spec/helpers/safe_format_helper_spec.rb b/spec/helpers/safe_format_helper_spec.rb index 33c4c86ecc8..3639494060d 100644 --- a/spec/helpers/safe_format_helper_spec.rb +++ b/spec/helpers/safe_format_helper_spec.rb @@ -27,15 +27,8 @@ RSpec.describe SafeFormatHelper, feature_category: :shared do result: '<b>strong</b> <a href="">link</a>' 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: '<b>strong</b>' end context 'with a view component' do @@ -54,5 +47,19 @@ RSpec.describe SafeFormatHelper, feature_category: :shared do .to eq('<b><br></b>') end end + + context 'with format containing escaped entities' do + it_behaves_like 'safe formatting', 'In < hour', + args: {}, + result: 'In < hour' + + it_behaves_like 'safe formatting', '"air"', + args: {}, + result: '"air"' + + it_behaves_like 'safe formatting', 'Mix & match > all', + args: {}, + result: 'Mix & match > all' + end end end diff --git a/spec/lib/gitlab/background_migration/remove_invalid_deploy_access_level_groups_spec.rb b/spec/lib/gitlab/background_migration/remove_invalid_deploy_access_level_groups_spec.rb new file mode 100644 index 00000000000..0cdfe7bb945 --- /dev/null +++ b/spec/lib/gitlab/background_migration/remove_invalid_deploy_access_level_groups_spec.rb @@ -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 diff --git a/spec/lib/gitlab/patch/redis_cache_store_spec.rb b/spec/lib/gitlab/patch/redis_cache_store_spec.rb index e1bf774d157..fdb220276a4 100644 --- a/spec/lib/gitlab/patch/redis_cache_store_spec.rb +++ b/spec/lib/gitlab/patch/redis_cache_store_spec.rb @@ -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 diff --git a/spec/lib/gitlab/redis/rate_limiting_spec.rb b/spec/lib/gitlab/redis/rate_limiting_spec.rb index 0bea7f8bcb2..e79c070df93 100644 --- a/spec/lib/gitlab/redis/rate_limiting_spec.rb +++ b/spec/lib/gitlab/redis/rate_limiting_spec.rb @@ -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 diff --git a/spec/mailers/emails/service_desk_spec.rb b/spec/mailers/emails/service_desk_spec.rb index c50d5ce2571..f8ed26b3241 100644 --- a/spec/mailers/emails/service_desk_spec.rb +++ b/spec/mailers/emails/service_desk_spec.rb @@ -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 diff --git a/spec/migrations/20230519011151_schedule_to_remove_invalid_deploy_access_level_groups_spec.rb b/spec/migrations/20230519011151_schedule_to_remove_invalid_deploy_access_level_groups_spec.rb new file mode 100644 index 00000000000..d5a20a8a7fe --- /dev/null +++ b/spec/migrations/20230519011151_schedule_to_remove_invalid_deploy_access_level_groups_spec.rb @@ -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 diff --git a/spec/models/customer_relations/organization_spec.rb b/spec/models/customer_relations/organization_spec.rb index 7fab9fd0e80..350a4e613c6 100644 --- a/spec/models/customer_relations/organization_spec.rb +++ b/spec/models/customer_relations/organization_spec.rb @@ -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 diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index f7d0695b757..d59c8e253ce 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -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 diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb index d6eb060ea7e..8ee9a75bdf0 100644 --- a/spec/services/groups/transfer_service_spec.rb +++ b/spec/services/groups/transfer_service_spec.rb @@ -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 diff --git a/spec/support/shared_examples/features/milestone_showing_shared_examples.rb b/spec/support/shared_examples/features/milestone_showing_shared_examples.rb new file mode 100644 index 00000000000..7bcaf1fe64a --- /dev/null +++ b/spec/support/shared_examples/features/milestone_showing_shared_examples.rb @@ -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 diff --git a/yarn.lock b/yarn.lock index 3f8333fbf76..8b17d2637b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" |