diff options
94 files changed, 837 insertions, 652 deletions
diff --git a/.markdownlint.json b/.markdownlint.json index 253b7bd0c92..1c276dc42ba 100644 --- a/.markdownlint.json +++ b/.markdownlint.json @@ -7,6 +7,7 @@ "ul-style": { "style": "dash" }, + "no-trailing-spaces": false, "line-length": false, "no-duplicate-header": { "allow_different_nesting": true diff --git a/app/assets/javascripts/design_management/utils/cache_update.js b/app/assets/javascripts/design_management/utils/cache_update.js index 3e6ed39fbe8..5df6e064b25 100644 --- a/app/assets/javascripts/design_management/utils/cache_update.js +++ b/app/assets/javascripts/design_management/utils/cache_update.js @@ -1,5 +1,6 @@ /* eslint-disable @gitlab/require-i18n-strings */ +import { groupBy } from 'lodash'; import createFlash from '~/flash'; import { extractCurrentDiscussion, extractDesign } from './design_management_utils'; import { @@ -159,13 +160,11 @@ const updateImageDiffNoteInStore = (store, updateImageDiffNote, query, variables const addNewDesignToStore = (store, designManagementUpload, query) => { const data = store.readQuery(query); - const newDesigns = data.project.issue.designCollection.designs.nodes.reduce((acc, design) => { - if (!acc.find(d => d.filename === design.filename)) { - acc.push(design); - } - - return acc; - }, designManagementUpload.designs); + const currentDesigns = data.project.issue.designCollection.designs.nodes; + const existingDesigns = groupBy(currentDesigns, 'filename'); + const newDesigns = currentDesigns.concat( + designManagementUpload.designs.filter(d => !existingDesigns[d.filename]), + ); let newVersionNode; const findNewVersions = designManagementUpload.designs.find(design => design.versions); diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue index 741462a849c..087a558efdc 100644 --- a/app/assets/javascripts/diffs/components/diff_content.vue +++ b/app/assets/javascripts/diffs/components/diff_content.vue @@ -147,7 +147,7 @@ export default { slot="image-overlay" :discussions="imageDiscussions" :file-hash="diffFileHash" - :can-comment="getNoteableData.current_user.can_create_note" + :can-comment="getNoteableData.current_user.can_create_note && !diffFile.brokenSymlink" /> <div v-if="showNotesContainer" class="note-container"> <user-avatar-link diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue index 58a4a3981bf..623a4ae1f8f 100644 --- a/app/assets/javascripts/diffs/components/diff_file.vue +++ b/app/assets/javascripts/diffs/components/diff_file.vue @@ -167,6 +167,7 @@ export default { :id="file.file_hash" :class="{ 'is-active': currentDiffFileId === file.file_hash, + 'comments-disabled': Boolean(file.brokenSymlink), }" :data-path="file.new_path" class="diff-file file-holder" diff --git a/app/assets/javascripts/diffs/components/diff_table_cell.vue b/app/assets/javascripts/diffs/components/diff_table_cell.vue index 198113e330a..49982a81372 100644 --- a/app/assets/javascripts/diffs/components/diff_table_cell.vue +++ b/app/assets/javascripts/diffs/components/diff_table_cell.vue @@ -1,8 +1,9 @@ <script> import { mapGetters, mapActions } from 'vuex'; -import { GlIcon } from '@gitlab/ui'; +import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils'; import DiffGutterAvatars from './diff_gutter_avatars.vue'; +import { __ } from '~/locale'; import { CONTEXT_LINE_TYPE, LINE_POSITION_RIGHT, @@ -18,6 +19,9 @@ export default { DiffGutterAvatars, GlIcon, }, + directives: { + GlTooltip: GlTooltipDirective, + }, props: { line: { type: Object, @@ -123,6 +127,24 @@ export default { lineNumber() { return this.lineType === OLD_LINE_TYPE ? this.line.old_line : this.line.new_line; }, + addCommentTooltip() { + const brokenSymlinks = this.line.commentsDisabled; + let tooltip = __('Add a comment to this line'); + + if (brokenSymlinks) { + if (brokenSymlinks.wasSymbolic || brokenSymlinks.isSymbolic) { + tooltip = __( + 'Commenting on symbolic links that replace or are replaced by files is currently not supported.', + ); + } else if (brokenSymlinks.wasReal || brokenSymlinks.isReal) { + tooltip = __( + 'Commenting on files that replace or are replaced by symbolic links is currently not supported.', + ); + } + } + + return tooltip; + }, }, mounted() { this.unwatchShouldShowCommentButton = this.$watch('shouldShowCommentButton', newVal => { @@ -146,17 +168,24 @@ export default { <template> <td ref="td" :class="classNameMap"> - <button - v-if="shouldRenderCommentButton" - v-show="shouldShowCommentButton" - ref="addDiffNoteButton" - type="button" - class="add-diff-note js-add-diff-note-button qa-diff-comment" - title="Add a comment to this line" - @click="handleCommentButton" + <span + ref="addNoteTooltip" + v-gl-tooltip + class="add-diff-note tooltip-wrapper" + :title="addCommentTooltip" > - <gl-icon :size="12" name="comment" /> - </button> + <button + v-if="shouldRenderCommentButton" + v-show="shouldShowCommentButton" + ref="addDiffNoteButton" + type="button" + class="add-diff-note note-button js-add-diff-note-button qa-diff-comment" + :disabled="line.commentsDisabled" + @click="handleCommentButton" + > + <gl-icon :size="12" name="comment" /> + </button> + </span> <a v-if="lineNumber" ref="lineNumberRef" diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js index 2d87008d3b1..f014cddda32 100644 --- a/app/assets/javascripts/diffs/store/utils.js +++ b/app/assets/javascripts/diffs/store/utils.js @@ -480,6 +480,10 @@ export function getDiffPositionByLineCode(diffFiles, useSingleDiffStyle) { // This method will check whether the discussion is still applicable // to the diff line in question regarding different versions of the MR export function isDiscussionApplicableToLine({ discussion, diffPosition, latestDiff }) { + if (!diffPosition) { + return false; + } + const { line_code, ...dp } = diffPosition; // Removing `line_range` from diffPosition because the backend does not // yet consistently return this property. This check can be removed, diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js index 5298e20557d..f0951f6b177 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js +++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js @@ -1,5 +1,5 @@ import VisualTokenValue from './visual_token_value'; -import { objectToQueryString } from '~/lib/utils/common_utils'; +import { objectToQueryString, spriteIcon } from '~/lib/utils/common_utils'; import FilteredSearchContainer from './container'; export default class FilteredSearchVisualTokens { @@ -84,7 +84,7 @@ export default class FilteredSearchVisualTokens { <div class="value-container"> <div class="${capitalizeTokenValue ? 'text-capitalize' : ''} value"></div> <div class="remove-token" role="button"> - <i class="fa fa-close"></i> + ${spriteIcon('close', 's16 close-icon')} </div> </div> </div> diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue index 5cf574e1387..67a8f11b760 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue @@ -1,4 +1,5 @@ <script> +import { GlIcon } from '@gitlab/ui'; import TimeTrackingHelpState from './help_state.vue'; import TimeTrackingCollapsedState from './collapsed_state.vue'; import TimeTrackingSpentOnlyPane from './spent_only_pane.vue'; @@ -11,6 +12,7 @@ import eventHub from '../../event_hub'; export default { name: 'IssuableTimeTracker', components: { + GlIcon, TimeTrackingCollapsedState, TimeTrackingEstimateOnlyPane, TimeTrackingSpentOnlyPane, @@ -111,7 +113,7 @@ export default { class="close-help-button float-right" @click="toggleHelpState(false)" > - <i class="fa fa-close" aria-hidden="true"> </i> + <gl-icon name="close" /> </div> </div> <div class="time-tracking-content hide-collapsed"> diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js index dd1da847001..c08659919fa 100644 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js @@ -1,13 +1,11 @@ import { __ } from '~/locale'; -import { generateToolbarItem } from './services/editor_service'; -import buildCustomHTMLRenderer from './services/build_custom_renderer'; export const CUSTOM_EVENTS = { openAddImageModal: 'gl_openAddImageModal', }; /* eslint-disable @gitlab/require-i18n-strings */ -const TOOLBAR_ITEM_CONFIGS = [ +export const TOOLBAR_ITEM_CONFIGS = [ { icon: 'heading', event: 'openHeadingSelect', classes: 'tui-heading', tooltip: __('Headings') }, { icon: 'bold', command: 'Bold', tooltip: __('Add bold text') }, { icon: 'italic', command: 'Italic', tooltip: __('Add italic text') }, @@ -30,11 +28,6 @@ const TOOLBAR_ITEM_CONFIGS = [ { icon: 'doc-code', command: 'CodeBlock', tooltip: __('Insert a code block') }, ]; -export const EDITOR_OPTIONS = { - toolbarItems: TOOLBAR_ITEM_CONFIGS.map(config => generateToolbarItem(config)), - customHTMLRenderer: buildCustomHTMLRenderer(), -}; - export const EDITOR_TYPES = { markdown: 'markdown', wysiwyg: 'wysiwyg', diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue b/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue index baeb98bec75..d96fe46522e 100644 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue @@ -3,16 +3,11 @@ import 'codemirror/lib/codemirror.css'; import '@toast-ui/editor/dist/toastui-editor.css'; import AddImageModal from './modals/add_image/add_image_modal.vue'; -import { - EDITOR_OPTIONS, - EDITOR_TYPES, - EDITOR_HEIGHT, - EDITOR_PREVIEW_STYLE, - CUSTOM_EVENTS, -} from './constants'; +import { EDITOR_TYPES, EDITOR_HEIGHT, EDITOR_PREVIEW_STYLE, CUSTOM_EVENTS } from './constants'; import { registerHTMLToMarkdownRenderer, + getEditorOptions, addCustomEventListener, removeCustomEventListener, addImage, @@ -35,7 +30,7 @@ export default { options: { type: Object, required: false, - default: () => EDITOR_OPTIONS, + default: () => null, }, initialEditType: { type: String, @@ -65,13 +60,13 @@ export default { }; }, computed: { - editorOptions() { - return { ...EDITOR_OPTIONS, ...this.options }; - }, editorInstance() { return this.$refs.editor; }, }, + created() { + this.editorOptions = getEditorOptions(this.options); + }, beforeDestroy() { this.removeListeners(); }, diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js index b210ada412d..a9c5d442f62 100644 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js @@ -1,3 +1,4 @@ +import { union, mapValues } from 'lodash'; import renderBlockHtml from './renderers/render_html_block'; import renderKramdownList from './renderers/render_kramdown_list'; import renderKramdownText from './renderers/render_kramdown_text'; @@ -19,63 +20,20 @@ const executeRenderer = (renderers, node, context) => { return availableRenderer ? availableRenderer.render(node, context) : context.origin(); }; -const buildCustomRendererFunctions = (customRenderers, defaults) => { - const customTypes = Object.keys(customRenderers).filter(type => !defaults[type]); - const customEntries = customTypes.map(type => { - const fn = (node, context) => executeRenderer(customRenderers[type], node, context); - return [type, fn]; - }); - - return Object.fromEntries(customEntries); -}; - -const buildCustomHTMLRenderer = ( - customRenderers = { - htmlBlock: [], - htmlInline: [], - list: [], - paragraph: [], - text: [], - softbreak: [], - }, -) => { - const defaults = { - htmlBlock(node, context) { - const allHtmlBlockRenderers = [...customRenderers.htmlBlock, ...htmlBlockRenderers]; - - return executeRenderer(allHtmlBlockRenderers, node, context); - }, - htmlInline(node, context) { - const allHtmlInlineRenderers = [...customRenderers.htmlInline, ...htmlInlineRenderers]; - - return executeRenderer(allHtmlInlineRenderers, node, context); - }, - list(node, context) { - const allListRenderers = [...customRenderers.list, ...listRenderers]; - - return executeRenderer(allListRenderers, node, context); - }, - paragraph(node, context) { - const allParagraphRenderers = [...customRenderers.paragraph, ...paragraphRenderers]; - - return executeRenderer(allParagraphRenderers, node, context); - }, - text(node, context) { - const allTextRenderers = [...customRenderers.text, ...textRenderers]; - - return executeRenderer(allTextRenderers, node, context); - }, - softbreak(node, context) { - const allSoftbreakRenderers = [...customRenderers.softbreak, ...softbreakRenderers]; - - return executeRenderer(allSoftbreakRenderers, node, context); - }, +const buildCustomHTMLRenderer = customRenderers => { + const renderersByType = { + ...customRenderers, + htmlBlock: union(htmlBlockRenderers, customRenderers?.htmlBlock), + htmlInline: union(htmlInlineRenderers, customRenderers?.htmlInline), + list: union(listRenderers, customRenderers?.list), + paragraph: union(paragraphRenderers, customRenderers?.paragraph), + text: union(textRenderers, customRenderers?.text), + softbreak: union(softbreakRenderers, customRenderers?.softbreak), }; - return { - ...buildCustomRendererFunctions(customRenderers, defaults), - ...defaults, - }; + return mapValues(renderersByType, renderers => { + return (node, context) => executeRenderer(renderers, node, context); + }); }; export default buildCustomHTMLRenderer; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js index 6436dcaae64..51ba033dff0 100644 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js @@ -1,6 +1,9 @@ import Vue from 'vue'; +import { defaults } from 'lodash'; import ToolbarItem from '../toolbar_item.vue'; import buildHtmlToMarkdownRenderer from './build_html_to_markdown_renderer'; +import buildCustomHTMLRenderer from './build_custom_renderer'; +import { TOOLBAR_ITEM_CONFIGS } from '../constants'; const buildWrapper = propsData => { const instance = new Vue({ @@ -54,3 +57,10 @@ export const registerHTMLToMarkdownRenderer = editorApi => { renderer: renderer.constructor.factory(renderer, buildHtmlToMarkdownRenderer(renderer)), }); }; + +export const getEditorOptions = externalOptions => { + return defaults({ + customHTMLRenderer: buildCustomHTMLRenderer(externalOptions?.customRenderers), + toolbarItems: TOOLBAR_ITEM_CONFIGS.map(toolbarItem => generateToolbarItem(toolbarItem)), + }); +}; diff --git a/app/assets/stylesheets/fontawesome_custom.scss b/app/assets/stylesheets/fontawesome_custom.scss index a96a002b879..28bea9233cb 100644 --- a/app/assets/stylesheets/fontawesome_custom.scss +++ b/app/assets/stylesheets/fontawesome_custom.scss @@ -93,7 +93,6 @@ } .fa-remove::before, -.fa-close::before, .fa-times::before { content: '\f00d'; } diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index ee83ce67c03..2e87724a9e7 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -134,20 +134,20 @@ padding-left: 8px; padding-right: 0; - .fa-close { + .close-icon { color: $gl-text-color-secondary; } - &:hover .fa-close { + &:hover .close-icon { color: $gl-text-color; } &.inverted { - .fa-close { + .close-icon { color: $gl-text-color-secondary-inverted; } - &:hover .fa-close { + &:hover .close-icon { color: $gl-text-color-inverted; } } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 46ea3c37f2d..e4e54501627 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -820,9 +820,7 @@ $note-form-margin-left: 72px; } } -.add-diff-note { - @include btn-comment-icon; - opacity: 0; +.tooltip-wrapper.add-diff-note { margin-left: -52px; position: absolute; top: 50%; @@ -830,6 +828,18 @@ $note-form-margin-left: 72px; z-index: 10; } +.note-button.add-diff-note { + @include btn-comment-icon; + opacity: 0; + + &[disabled] { + background: $white; + border-color: $gray-200; + color: $gl-gray-400; + cursor: not-allowed; + } +} + .disabled-comment { background-color: $gray-light; border-radius: $border-radius-base; diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index bbb37479fb0..c6f104a024b 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -143,6 +143,10 @@ margin-bottom: 0; } } + + .gl-label-scoped { + --label-inset-border: inset 0 0 0 1px currentColor; + } } @include media-breakpoint-down(sm) { diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb index 2cc030d18fc..0fd047f90cf 100644 --- a/app/controllers/projects/variables_controller.rb +++ b/app/controllers/projects/variables_controller.rb @@ -12,7 +12,12 @@ class Projects::VariablesController < Projects::ApplicationController end def update - if @project.update(variables_params) + update_result = Ci::ChangeVariablesService.new( + container: @project, current_user: current_user, + params: variables_params + ).execute + + if update_result respond_to do |format| format.json { render_variables } end diff --git a/app/graphql/mutations/design_management/move.rb b/app/graphql/mutations/design_management/move.rb index c103e596387..0b654447844 100644 --- a/app/graphql/mutations/design_management/move.rb +++ b/app/graphql/mutations/design_management/move.rb @@ -21,7 +21,7 @@ module Mutations description: "The current state of the collection" def ready(*) - raise ::Gitlab::Graphql::Errors::ResourceNotAvailable unless ::Feature.enabled?(:reorder_designs) + raise ::Gitlab::Graphql::Errors::ResourceNotAvailable unless ::Feature.enabled?(:reorder_designs, default_enabled: true) end def resolve(**args) diff --git a/app/models/design_management/design.rb b/app/models/design_management/design.rb index 865cbb995b5..4e50c048611 100644 --- a/app/models/design_management/design.rb +++ b/app/models/design_management/design.rb @@ -82,7 +82,7 @@ module DesignManagement scope :ordered, -> (project) do # TODO: Always order by relative position after the feature flag is removed # https://gitlab.com/gitlab-org/gitlab/-/issues/34382 - if Feature.enabled?(:reorder_designs, project) + if Feature.enabled?(:reorder_designs, project, default_enabled: true) # We need to additionally sort by `id` to support keyset pagination. # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/17788/diffs#note_230875678 order(:relative_position, :id) diff --git a/app/models/design_management/design_collection.rb b/app/models/design_management/design_collection.rb index 5a4322a2994..96d5f4c2419 100644 --- a/app/models/design_management/design_collection.rb +++ b/app/models/design_management/design_collection.rb @@ -16,9 +16,7 @@ module DesignManagement def find_or_create_design!(filename:) designs.find { |design| design.filename == filename } || - designs.safe_find_or_create_by!(project: project, filename: filename) do |design| - design.move_to_end - end + designs.safe_find_or_create_by!(project: project, filename: filename) end def versions diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index c5d70f3f207..36d7026de30 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -8,6 +8,12 @@ class JiraService < IssueTrackerService PROJECTS_PER_PAGE = 50 + # TODO: use jira_service.deployment_type enum when https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37003 is merged + DEPLOYMENT_TYPES = { + server: 'SERVER', + cloud: 'CLOUD' + }.freeze + validates :url, public_url: true, presence: true, if: :activated? validates :api_url, public_url: true, allow_blank: true validates :username, presence: true, if: :activated? diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index bdfad994d55..21a73185df4 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -103,7 +103,7 @@ class ProjectPolicy < BasePolicy with_scope :subject condition(:moving_designs_disabled) do - !::Feature.enabled?(:reorder_designs, @subject) + !::Feature.enabled?(:reorder_designs, @subject, default_enabled: true) end with_scope :subject diff --git a/app/presenters/packages/detail/package_presenter.rb b/app/presenters/packages/detail/package_presenter.rb index 631ee173188..bdb2e34854e 100644 --- a/app/presenters/packages/detail/package_presenter.rb +++ b/app/presenters/packages/detail/package_presenter.rb @@ -23,6 +23,7 @@ module Packages package_detail[:maven_metadatum] = @package.maven_metadatum if @package.maven_metadatum package_detail[:nuget_metadatum] = @package.nuget_metadatum if @package.nuget_metadatum package_detail[:composer_metadatum] = @package.composer_metadatum if @package.composer_metadatum + package_detail[:conan_metadatum] = @package.conan_metadatum if @package.conan_metadatum package_detail[:dependency_links] = @package.dependency_links.map(&method(:build_dependency_links)) package_detail[:pipeline] = build_pipeline_info(@package.build_info.pipeline) if @package.build_info diff --git a/app/services/ci/change_variable_service.rb b/app/services/ci/change_variable_service.rb index 6588ad598f9..f515a335d54 100644 --- a/app/services/ci/change_variable_service.rb +++ b/app/services/ci/change_variable_service.rb @@ -20,7 +20,12 @@ module Ci private def variable - container.variables.find_by!(params[:variable_params].slice(:key)) # rubocop:disable CodeReuse/ActiveRecord + params[:variable] || find_variable + end + + def find_variable + identifier = params[:variable_params].slice(:id).presence || params[:variable_params].slice(:key) + container.variables.find_by!(identifier) # rubocop:disable CodeReuse/ActiveRecord end end end diff --git a/app/services/design_management/move_designs_service.rb b/app/services/design_management/move_designs_service.rb index 4052c189969..ead41e8f4d4 100644 --- a/app/services/design_management/move_designs_service.rb +++ b/app/services/design_management/move_designs_service.rb @@ -13,7 +13,7 @@ module DesignManagement def execute return error(:no_focus) unless current_design.present? - return error(:cannot_move) unless ::Feature.enabled?(:reorder_designs, project) + return error(:cannot_move) unless ::Feature.enabled?(:reorder_designs, project, default_enabled: true) return error(:cannot_move) unless current_user.can?(:move_design, current_design) return error(:no_neighbors) unless neighbors.present? return error(:not_distinct) unless all_distinct? diff --git a/app/services/jira_import/cloud_users_mapper_service.rb b/app/services/jira_import/cloud_users_mapper_service.rb new file mode 100644 index 00000000000..b1c7aac584f --- /dev/null +++ b/app/services/jira_import/cloud_users_mapper_service.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module JiraImport + class CloudUsersMapperService < UsersMapperService + private + + def url + "/rest/api/2/users?maxResults=#{MAX_USERS}&startAt=#{start_at.to_i}" + end + + def jira_user_id(jira_user) + jira_user['accountId'] + end + + def jira_user_name(jira_user) + jira_user['displayName'] + end + end +end diff --git a/app/services/jira_import/server_users_mapper_service.rb b/app/services/jira_import/server_users_mapper_service.rb new file mode 100644 index 00000000000..d38d134f55c --- /dev/null +++ b/app/services/jira_import/server_users_mapper_service.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module JiraImport + class ServerUsersMapperService < UsersMapperService + private + + def url + "/rest/api/2/user/search?username=''&maxResults=#{MAX_USERS}&startAt=#{start_at.to_i}" + end + + def jira_user_id(jira_user) + jira_user['key'] + end + + def jira_user_name(jira_user) + jira_user['name'] + end + end +end diff --git a/app/services/jira_import/users_importer.rb b/app/services/jira_import/users_importer.rb index 579d3675073..9babd468d56 100644 --- a/app/services/jira_import/users_importer.rb +++ b/app/services/jira_import/users_importer.rb @@ -2,9 +2,7 @@ module JiraImport class UsersImporter - attr_reader :user, :project, :start_at, :result - - MAX_USERS = 50 + attr_reader :user, :project, :start_at def initialize(user, project, start_at) @project = project @@ -15,29 +13,43 @@ module JiraImport def execute Gitlab::JiraImport.validate_project_settings!(project, user: user) - return ServiceResponse.success(payload: nil) if users.blank? - - result = UsersMapper.new(project, users).execute - ServiceResponse.success(payload: result) + ServiceResponse.success(payload: mapped_users) rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, URI::InvalidURIError, JIRA::HTTPError, OpenSSL::SSL::SSLError => error - Gitlab::ErrorTracking.track_exception(error, project_id: project.id, request: url) - ServiceResponse.error(message: "There was an error when communicating to Jira: #{error.message}") + Gitlab::ErrorTracking.track_exception(error, project_id: project.id) + ServiceResponse.error(message: "There was an error when communicating to Jira") rescue Projects::ImportService::Error => error ServiceResponse.error(message: error.message) end private - def users - @users ||= client.get(url) + def mapped_users + users_mapper_service.execute + end + + def users_mapper_service + @users_mapper_service ||= user_mapper_service_factory end - def url - "/rest/api/2/users?maxResults=#{MAX_USERS}&startAt=#{start_at.to_i}" + def deployment_type + # TODO: use project.jira_service.deployment_type value when https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37003 is merged + @deployment_type ||= client.ServerInfo.all.deploymentType end def client @client ||= project.jira_service.client end + + def user_mapper_service_factory + # TODO: use deployment_type enum from jira service when https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37003 is merged + case deployment_type.upcase + when JiraService::DEPLOYMENT_TYPES[:server] + ServerUsersMapperService.new(project.jira_service, start_at) + when JiraService::DEPLOYMENT_TYPES[:cloud] + CloudUsersMapperService.new(project.jira_service, start_at) + else + raise ArgumentError + end + end end end diff --git a/app/services/jira_import/users_mapper.rb b/app/services/jira_import/users_mapper.rb deleted file mode 100644 index c3cbeb157bd..00000000000 --- a/app/services/jira_import/users_mapper.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -module JiraImport - class UsersMapper - attr_reader :project, :jira_users - - def initialize(project, jira_users) - @project = project - @jira_users = jira_users - end - - def execute - jira_users.to_a.map do |jira_user| - { - jira_account_id: jira_user['accountId'], - jira_display_name: jira_user['displayName'], - jira_email: jira_user['emailAddress'] - }.merge(match_user(jira_user)) - end - end - - private - - # TODO: Matching user by email and displayName will be done as the part - # of follow-up issue: https://gitlab.com/gitlab-org/gitlab/-/issues/219023 - def match_user(jira_user) - { gitlab_id: nil, gitlab_username: nil, gitlab_name: nil } - end - end -end diff --git a/app/services/jira_import/users_mapper_service.rb b/app/services/jira_import/users_mapper_service.rb new file mode 100644 index 00000000000..b5997d77215 --- /dev/null +++ b/app/services/jira_import/users_mapper_service.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module JiraImport + class UsersMapperService + MAX_USERS = 50 + + attr_reader :jira_service, :start_at + + def initialize(jira_service, start_at) + @jira_service = jira_service + @start_at = start_at + end + + def execute + users.to_a.map do |jira_user| + { + jira_account_id: jira_user_id(jira_user), + jira_display_name: jira_user_name(jira_user), + jira_email: jira_user['emailAddress'] + }.merge(match_user(jira_user)) + end + end + + private + + def users + @users ||= client.get(url) + end + + def client + @client ||= jira_service.client + end + + def url + raise NotImplementedError + end + + def jira_user_id(jira_user) + raise NotImplementedError + end + + def jira_user_name(jira_user) + raise NotImplementedError + end + + # TODO: Matching user by email and displayName will be done as the part + # of follow-up issue: https://gitlab.com/gitlab-org/gitlab/-/issues/219023 + def match_user(jira_user) + { gitlab_id: nil, gitlab_username: nil, gitlab_name: nil } + end + end +end diff --git a/bin/feature-flag b/bin/feature-flag index d4ab295b30c..ba86df9c06c 100755 --- a/bin/feature-flag +++ b/bin/feature-flag @@ -8,7 +8,7 @@ require 'optparse' require 'yaml' require 'fileutils' -require 'cgi' +require 'uri' require_relative '../lib/feature/shared' unless defined?(Feature::Shared) @@ -105,10 +105,11 @@ class FeatureFlagOptionParser end def read_group + $stdout.puts $stdout.puts ">> Please specify the group introducing feature flag, like `group::apm`:" loop do - $stdout.print "\n?> " + $stdout.print "?> " group = $stdin.gets.strip group = nil if group.empty? return group if group.nil? || group.start_with?('group::') @@ -121,6 +122,7 @@ class FeatureFlagOptionParser # if there's only one type, do not ask, return return TYPES.first.first if TYPES.one? + $stdout.puts $stdout.puts ">> Please specify the type of your feature flag:" $stdout.puts TYPES.each do |type, data| @@ -128,7 +130,7 @@ class FeatureFlagOptionParser end loop do - $stdout.print "\n?> " + $stdout.print "?> " type = $stdin.gets.strip.to_sym return type if TYPES[type] @@ -137,27 +139,41 @@ class FeatureFlagOptionParser end end - def read_issue_url(options) + def read_introduced_by_url + $stdout.puts + $stdout.puts ">> If you have MR open, can you paste the URL here? (or enter to skip)" + + loop do + $stdout.print "?> " + introduced_by_url = $stdin.gets.strip + introduced_by_url = nil if introduced_by_url.empty? + return introduced_by_url if introduced_by_url.nil? || introduced_by_url.start_with?('https://') + + $stderr.puts "URL needs to start with https://" + end + end + + def read_rollout_issue_url(options) return unless TYPES.dig(options.type, :rollout_issue) url = "https://gitlab.com/gitlab-org/gitlab/-/issues/new" title = "[Feature flag] Rollout of `#{options.name}`" - description = File.read('.gitlab/issue_templates/Feature Flag Roll Out.md') - description.sub!(':feature_name', options.name) - issue_new_url = url + "?" + - "issue[title]=" + CGI.escape(title) + "&" - # TODO: We should be able to pick `issueable_template` - # + "issue[description]=" + CGI.escape(description) + params = { + 'issue[title]' => "[Feature flag] Rollout of `#{options.name}`", + 'issuable_template' => 'Feature Flag Roll Out', + } + issue_new_url = url + "?" + URI.encode_www_form(params) + $stdout.puts $stdout.puts ">> Open this URL and fill the rest of details:" $stdout.puts issue_new_url $stdout.puts - $stdout.puts ">> Paste URL here, or enter to skip:" + $stdout.puts ">> Paste URL of `rollout issue` here, or enter to skip:" loop do - $stdout.print "\n?> " + $stdout.print "?> " created_url = $stdin.gets.strip created_url = nil if created_url.empty? return created_url if created_url.nil? || created_url.start_with?('https://') @@ -185,7 +201,8 @@ class FeatureFlagCreator # Read type from $stdin unless is already set options.type ||= FeatureFlagOptionParser.read_type options.group ||= FeatureFlagOptionParser.read_group - options.rollout_issue_url ||= FeatureFlagOptionParser.read_issue_url(options) + options.introduced_by_url ||= FeatureFlagOptionParser.read_introduced_by_url + options.rollout_issue_url ||= FeatureFlagOptionParser.read_rollout_issue_url(options) $stdout.puts "\e[32mcreate\e[0m #{file_path}" $stdout.puts contents diff --git a/changelogs/unreleased/225946-replace-fa-close-with-gitlab-svg.yml b/changelogs/unreleased/225946-replace-fa-close-with-gitlab-svg.yml new file mode 100644 index 00000000000..ea78201234d --- /dev/null +++ b/changelogs/unreleased/225946-replace-fa-close-with-gitlab-svg.yml @@ -0,0 +1,5 @@ +--- +title: Replace fa-close icons with GitLab SVG close icon +merge_request: 39267 +author: +type: changed diff --git a/changelogs/unreleased/232998-package-metadata.yml b/changelogs/unreleased/232998-package-metadata.yml new file mode 100644 index 00000000000..038e84e9c8d --- /dev/null +++ b/changelogs/unreleased/232998-package-metadata.yml @@ -0,0 +1,5 @@ +--- +title: Fix Conan recipe display in the package details page +merge_request: 39643 +author: +type: fixed diff --git a/changelogs/unreleased/235759_backward_compatibility_on_jira_users_api_endpoint.yml b/changelogs/unreleased/235759_backward_compatibility_on_jira_users_api_endpoint.yml new file mode 100644 index 00000000000..aadad6fad88 --- /dev/null +++ b/changelogs/unreleased/235759_backward_compatibility_on_jira_users_api_endpoint.yml @@ -0,0 +1,5 @@ +--- +title: Handle user mapping for Jira server instances +merge_request: 39362 +author: +type: fixed diff --git a/changelogs/unreleased/235797-fix-issues-analytics-feature-name.yml b/changelogs/unreleased/235797-fix-issues-analytics-feature-name.yml new file mode 100644 index 00000000000..2221ab750b6 --- /dev/null +++ b/changelogs/unreleased/235797-fix-issues-analytics-feature-name.yml @@ -0,0 +1,5 @@ +--- +title: Deprecate additions and deletions attributes in Repositories API +merge_request: 39653 +author: +type: deprecated diff --git a/changelogs/unreleased/reorder-designs-fixes.yml b/changelogs/unreleased/reorder-designs-fixes.yml new file mode 100644 index 00000000000..05d2fc4ca7e --- /dev/null +++ b/changelogs/unreleased/reorder-designs-fixes.yml @@ -0,0 +1,5 @@ +--- +title: Enable reorder_designs feature by default +merge_request: 39555 +author: +type: changed diff --git a/changelogs/unreleased/symlink-comments-disable-symlink-comments.yml b/changelogs/unreleased/symlink-comments-disable-symlink-comments.yml new file mode 100644 index 00000000000..2e1d19ae2a9 --- /dev/null +++ b/changelogs/unreleased/symlink-comments-disable-symlink-comments.yml @@ -0,0 +1,6 @@ +--- +title: Disable commenting on lines in files that were or are symlinks or replace or + are replaced by symlinks +merge_request: 35371 +author: +type: fixed diff --git a/changelogs/unreleased/todo-labels.yml b/changelogs/unreleased/todo-labels.yml new file mode 100644 index 00000000000..5a33cc6cfbf --- /dev/null +++ b/changelogs/unreleased/todo-labels.yml @@ -0,0 +1,5 @@ +--- +title: Fix missing scoped label borders for todos +merge_request: 39459 +author: +type: fixed diff --git a/config/feature_flags/development/ci_if_parenthesis_enabled.yml b/config/feature_flags/development/ci_if_parenthesis_enabled.yml new file mode 100644 index 00000000000..5de7f9cf09a --- /dev/null +++ b/config/feature_flags/development/ci_if_parenthesis_enabled.yml @@ -0,0 +1,7 @@ +--- +name: ci_if_parenthesis_enabled +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37574 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/238174 +group: group::ci +type: development +default_enabled: true
\ No newline at end of file diff --git a/config/feature_flags/development/ci_plan_needs_size_limit.yml b/config/feature_flags/development/ci_plan_needs_size_limit.yml new file mode 100644 index 00000000000..826aeb8f030 --- /dev/null +++ b/config/feature_flags/development/ci_plan_needs_size_limit.yml @@ -0,0 +1,7 @@ +--- +name: ci_plan_needs_size_limit +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37568 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/238173 +group: group::ci +type: development +default_enabled: true
\ No newline at end of file diff --git a/config/feature_flags/development/reorder_designs.yml b/config/feature_flags/development/reorder_designs.yml index 3bb05ff743f..89c6bec7351 100644 --- a/config/feature_flags/development/reorder_designs.yml +++ b/config/feature_flags/development/reorder_designs.yml @@ -4,4 +4,4 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37835 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/232992 group: group::knowledge type: development -default_enabled: false +default_enabled: true diff --git a/config/initializers/rails_host_authorization.rb b/config/initializers/rails_host_authorization.rb index 6cca39ea95b..7d719dd519f 100644 --- a/config/initializers/rails_host_authorization.rb +++ b/config/initializers/rails_host_authorization.rb @@ -3,7 +3,7 @@ # This file requires config/initializers/1_settings.rb if Rails.env.development? - Rails.application.config.hosts += [Gitlab.config.gitlab.host, 'unix'] + Rails.application.config.hosts += [Gitlab.config.gitlab.host, 'unix', 'host.docker.internal'] if ENV['RAILS_HOSTS'] additional_hosts = ENV['RAILS_HOSTS'].split(',').select(&:presence) diff --git a/db/post_migrate/20200724130639_backfill_designs_relative_position.rb b/db/post_migrate/20200724130639_backfill_designs_relative_position.rb index a090678214f..ef8b38bf90c 100644 --- a/db/post_migrate/20200724130639_backfill_designs_relative_position.rb +++ b/db/post_migrate/20200724130639_backfill_designs_relative_position.rb @@ -1,38 +1,19 @@ # frozen_string_literal: true +# This migration is not needed anymore and was disabled, because we're now +# also backfilling design positions immediately before moving a design. +# +# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39555 class BackfillDesignsRelativePosition < ActiveRecord::Migration[6.0] include Gitlab::Database::MigrationHelpers DOWNTIME = false - INTERVAL = 2.minutes - BATCH_SIZE = 1000 - MIGRATION = 'BackfillDesignsRelativePosition' - - disable_ddl_transaction! - - class Issue < ActiveRecord::Base - include EachBatch - - self.table_name = 'issues' - - has_many :designs - end - - class Design < ActiveRecord::Base - self.table_name = 'design_management_designs' - end def up - issues_with_designs = Issue.where(id: Design.select(:issue_id)) - - issues_with_designs.each_batch(of: BATCH_SIZE) do |relation, index| - issue_ids = relation.pluck(:id) - delay = INTERVAL * index - - migrate_in(delay, MIGRATION, [issue_ids]) - end + # no-op end def down + # no-op end end diff --git a/doc/api/repositories.md b/doc/api/repositories.md index 79ccd5bc9e4..305216f853a 100644 --- a/doc/api/repositories.md +++ b/doc/api/repositories.md @@ -199,6 +199,9 @@ authentication if the repository is publicly accessible. GET /projects/:id/repository/contributors ``` +CAUTION: **Deprecation:** +The `additions` and `deletions` attributes are deprecated [as of GitLab 13.4](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39653) because they [always return `0`](https://gitlab.com/gitlab-org/gitlab/-/issues/233119). + Parameters: - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user @@ -212,14 +215,14 @@ Response: "name": "Example User", "email": "example@example.com", "commits": 117, - "additions": 2097, - "deletions": 517 + "additions": 0, + "deletions": 0 }, { "name": "Sample User", "email": "sample@example.com", "commits": 33, - "additions": 338, - "deletions": 244 + "additions": 0, + "deletions": 0 }] ``` diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 8f137b6436a..61bc466692e 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -760,9 +760,9 @@ Examples: - `($VARIABLE1 =~ /^content.*/ || $VARIABLE2 =~ /thing$/) && $VARIABLE3` - `$CI_COMMIT_BRANCH == "my-branch" || (($VARIABLE1 == "thing" || $VARIABLE2 == "thing") && $VARIABLE3)` -The feature is currently deployed behind a feature flag that is **disabled by default**. +The feature is currently deployed behind a feature flag that is **enabled by default**. [GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md) -can opt to enable it for your instance. +can opt to disable it for your instance. To enable it: diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index e6e84fbc3cb..82a410129a6 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -1989,9 +1989,7 @@ This example creates four paths of execution: - The maximum number of jobs that a single job can need in the `needs:` array is limited: - For GitLab.com, the limit is ten. For more information, see our [infrastructure issue](https://gitlab.com/gitlab-com/gl-infra/infrastructure/-/issues/7541). - - For self-managed instances, the limit is: - - 10, if the `ci_plan_needs_size_limit` feature flag is disabled (default). - - 50, if the `ci_plan_needs_size_limit` feature flag is enabled. This limit [can be changed](#changing-the-needs-job-limit-core-only). + - For self-managed instances, the limit is: 50. This limit [can be changed](#changing-the-needs-job-limit-core-only). - If `needs:` refers to a job that is marked as `parallel:`. the current job will depend on all parallel jobs created. - `needs:` is similar to `dependencies:` in that it needs to use jobs from prior stages, @@ -2002,18 +2000,10 @@ This example creates four paths of execution: ##### Changing the `needs:` job limit **(CORE ONLY)** -The maximum number of jobs that can be defined within `needs:` defaults to 10. +The maximum number of jobs that can be defined within `needs:` defaults to 50. -To change this limit to 50 on a self-managed installation, a GitLab administrator -with [access to the GitLab Rails console](../../administration/feature_flags.md) -can enable the `:ci_plan_needs_size_limit` feature flag: - -```ruby -Feature::enable(:ci_plan_needs_size_limit) -``` - -After the feature flag is enabled, you can choose a custom limit. For example, to -set the limit to 100: +A GitLab administrator with [access to the GitLab Rails console](../../administration/feature_flags.md) +can choose a custom limit. For example, to set the limit to 100: ```ruby Plan.default.actual_limits.update!(ci_needs_size_limit: 100) diff --git a/doc/development/deleting_migrations.md b/doc/development/deleting_migrations.md index 8aa16710d55..b8f23019ac2 100644 --- a/doc/development/deleting_migrations.md +++ b/doc/development/deleting_migrations.md @@ -23,7 +23,7 @@ Migrations can be disabled if: In order to disable a migration, the following steps apply to all types of migrations: 1. Turn the migration into a no-op by removing the code inside `#up`, `#down` - or `#perform` methods, and adding `#no-op` comment instead. + or `#perform` methods, and adding `# no-op` comment instead. 1. Add a comment explaining why the code is gone. Disabling migrations requires explicit approval of Database Maintainer. diff --git a/doc/development/feature_flags/development.md b/doc/development/feature_flags/development.md index cb97a775409..9b30187fcd1 100644 --- a/doc/development/feature_flags/development.md +++ b/doc/development/feature_flags/development.md @@ -78,19 +78,21 @@ Only feature flags that have a YAML definition file can be used when running the ```shell $ bin/feature-flag my-feature-flag >> Please specify the group introducing feature flag, like `group::apm`: - ?> group::memory ->> Open this URL and fill the rest of details: -https://gitlab.com/gitlab-org/gitlab/-/issues/new?issue[title]=%5BFeature+flag%5D+Rollout+of+%60my-feature-flag%60& ->> Paste URL here, or enter to skip: +>> If you have MR open, can you paste the URL here? (or enter to skip) +?> https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38602 + +>> Open this URL and fill the rest of details: +https://gitlab.com/gitlab-org/gitlab/-/issues/new?issue%5Btitle%5D=%5BFeature+flag%5D+Rollout+of+%60test-flag%60&issuable_template=Feature+Flag+Roll+Out -?> -create config/feature_flags/development/my_feature_flag.yml +>> Paste URL of `rollout issue` here, or enter to skip: +?> https://gitlab.com/gitlab-org/gitlab/-/issues/232533 +create config/feature_flags/development/test-flag.yml --- -name: my_feature_flag -introduced_by_url: -rollout_issue_url: +name: test-flag +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38602 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/232533 group: group::memory type: development default_enabled: false diff --git a/doc/user/compliance/compliance_dashboard/img/compliance_dashboard_v13_3.png b/doc/user/compliance/compliance_dashboard/img/compliance_dashboard_v13_3.png Binary files differdeleted file mode 100644 index bf759f44dc5..00000000000 --- a/doc/user/compliance/compliance_dashboard/img/compliance_dashboard_v13_3.png +++ /dev/null diff --git a/doc/user/project/merge_requests/merge_request_approvals.md b/doc/user/project/merge_requests/merge_request_approvals.md index b8a4d513318..407fc5db425 100644 --- a/doc/user/project/merge_requests/merge_request_approvals.md +++ b/doc/user/project/merge_requests/merge_request_approvals.md @@ -1,7 +1,7 @@ --- stage: Create group: Source Code -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers" type: reference, concepts --- @@ -13,16 +13,16 @@ process, as it clearly communicates the ability to merge the change. ## Optional Approvals **(CORE ONLY)** -> Introduced in [GitLab Core 13.2](https://gitlab.com/gitlab-org/gitlab/-/issues/27426). +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/27426) in GitLab 13.2. -Any user with Developer or greater [permissions](../../permissions.md) can approve a merge request in GitLab Core. -This provides a consistent mechanism for reviewers to provide approval, and makes it easy for +Any user with Developer or greater [permissions](../../permissions.md) can approve a merge request in GitLab Core and higher tiers. +This provides a consistent mechanism for reviewers to approve merge requests, and makes it easy for maintainers to know when a change is ready to merge. Approvals in Core are optional and do not prevent a merge request from being merged when there is no approval. ## Required Approvals **(STARTER)** -> Introduced in [GitLab Enterprise Edition 7.12](https://about.gitlab.com/releases/2015/06/22/gitlab-7-12-released/#merge-request-approvers-ee-only). +> [Introduced](https://about.gitlab.com/releases/2015/06/22/gitlab-7-12-released/#merge-request-approvers-ee-only) in GitLab Enterprise Edition 7.12. Available in [GitLab Starter](https://about.gitlab.com/pricing/) and higher tiers. Required approvals enable enforced code review by requiring specified people to approve a merge request before it can be merged. @@ -33,8 +33,8 @@ Required approvals enable multiple use cases: - Specifying reviewers for a given proposed code change, as well as a minimum number of reviewers, through [Approval rules](#approval-rules). - Specifying categories of reviewers, such as backend, frontend, quality assurance, - database, etc., for all proposed code changes. -- Automatically designating [Code Owners as eligible approvers](#code-owners-as-eligible-approvers), + database, and so on, for all proposed code changes. +- Designating [Code Owners as eligible approvers](#code-owners-as-eligible-approvers), determined by the files changed in a merge request. - [Requiring approval from a security team](#security-approvals-in-merge-requests-ultimate) before merging code that could introduce a vulnerability.**(ULTIMATE)** @@ -50,14 +50,10 @@ be merged, and optionally which users should do the approving. Approvals can be If no approval rules are defined, any user can approve a merge request, though the default minimum number of required approvers can still be set in the [project settings for merge request approvals](#merge-request-approvals-project-settings). -Approval rules define how many approvals a merge request must receive before it can -be merged, and optionally which users should do the approving. Approvals can be defined: - -- [As project defaults](#adding--editing-a-default-approval-rule). -- [Per merge request](#editing--overriding-approval-rules-per-merge-request). - -If no approval rules are defined, any user can approve a merge request, though the default -minimum number of required approvers can still be set in the [project settings for merge request approvals](#merge-request-approvals-project-settings). +You can opt to define one single rule to approve a merge request among the available rules +or choose more than one. Single approval rules are available in GitLab Starter and higher tiers, +while [multiple approval rules](#multiple-approval-rules-premium) are available in +[GitLab Premium](https://about.gitlab.com/pricing/) and above. NOTE: **Note:** On GitLab.com, you can add a group as an approver if you're a member of that group or the @@ -88,6 +84,11 @@ if [**Prevent author approval**](#allowing-merge-request-authors-to-approve-thei and [**Prevent committers approval**](#prevent-approval-of-merge-requests-by-their-committers) (disabled by default) are enabled on the project settings. +[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/10294) in [GitLab Starter](https://about.gitlab.com/pricing/) 13.3, +when an eligible approver comments on a merge request, it appears in the **Commented by** column of the Approvals widget, +indicating who has engaged in the merge request review. Authors and reviewers can also easily identify who they should reach out +to if they have any questions or inputs about the content of the merge request. + ##### Implicit Approvers If the number of required approvals is greater than the number of assigned approvers, @@ -187,9 +188,6 @@ a rule is already defined. When an [eligible approver](#eligible-approvers) approves a merge request, it will reduce the number of approvals left for all rules that the approver belongs to. -When an [eligible approver](#eligible-approvers) comments on a merge request, it -appears in the **Commented by** column. This feature was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/10294) in GitLab 13.3. - ![Approvals premium merge request widget](img/approvals_premium_mr_widget_v13_3.png) #### Scoped to Protected Branch **(PREMIUM)** diff --git a/lib/api/variables.rb b/lib/api/variables.rb index 6f449fd060a..cea0cb3a19c 100644 --- a/lib/api/variables.rb +++ b/lib/api/variables.rb @@ -67,10 +67,11 @@ module API optional :environment_scope, type: String, desc: 'The environment_scope of the variable' end post ':id/variables' do - variable_params = declared_params(include_missing: false) - variable_params = filter_variable_parameters(variable_params) - - variable = user_project.variables.create(variable_params) + variable = ::Ci::ChangeVariableService.new( + container: user_project, + current_user: current_user, + params: { action: :create, variable_params: filter_variable_parameters(declared_params(include_missing: false)) } + ).execute if variable.valid? present variable, with: Entities::Ci::Variable @@ -96,10 +97,17 @@ module API variable = find_variable(params) not_found!('Variable') unless variable - variable_params = declared_params(include_missing: false).except(:key, :filter) - variable_params = filter_variable_parameters(variable_params) + variable_params = filter_variable_parameters( + declared_params(include_missing: false) + .except(:key, :filter) + ) + variable = ::Ci::ChangeVariableService.new( + container: user_project, + current_user: current_user, + params: { action: :update, variable: variable, variable_params: variable_params } + ).execute - if variable.update(variable_params) + if variable.valid? present variable, with: Entities::Ci::Variable else render_validation_error!(variable) @@ -119,8 +127,11 @@ module API variable = find_variable(params) not_found!('Variable') unless variable - # Variables don't have a timestamp. Therefore, destroy unconditionally. - variable.destroy + ::Ci::ChangeVariableService.new( + container: user_project, + current_user: current_user, + params: { action: :destroy, variable: variable } + ).execute no_content! end diff --git a/lib/gitlab/background_migration/backfill_designs_relative_position.rb b/lib/gitlab/background_migration/backfill_designs_relative_position.rb index 4cfaa899eef..efbb1b950ad 100644 --- a/lib/gitlab/background_migration/backfill_designs_relative_position.rb +++ b/lib/gitlab/background_migration/backfill_designs_relative_position.rb @@ -2,52 +2,13 @@ module Gitlab module BackgroundMigration - # Backfill `relative_position` column in `design_management_designs` table + # This migration is not needed anymore and was disabled, because we're now + # also backfilling design positions immediately before moving a design. + # + # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39555 class BackfillDesignsRelativePosition - # Define the issue model - class Issue < ActiveRecord::Base - self.table_name = 'issues' - end - - # Define the design model - class Design < ActiveRecord::Base - include RelativePositioning if defined?(RelativePositioning) - - self.table_name = 'design_management_designs' - - def self.relative_positioning_query_base(design) - where(issue_id: design.issue_id) - end - - def self.relative_positioning_parent_column - :issue_id - end - - def self.move_nulls_to_start(designs) - if defined?(super) - super(designs) - else - logger.error "BackfillDesignsRelativePosition failed because move_nulls_to_start is no longer included in the RelativePositioning concern" - end - end - end - def perform(issue_ids) - issue_ids.each do |issue_id| - migrate_issue(issue_id) - end - end - - private - - def migrate_issue(issue_id) - issue = Issue.find_by(id: issue_id) - return unless issue - - designs = Design.where(issue_id: issue.id).order(:id) - return unless designs.any? - - Design.move_nulls_to_start(designs) + # no-op end end end diff --git a/lib/gitlab/ci/features.rb b/lib/gitlab/ci/features.rb index 5d0e1e4fd01..38767acf716 100644 --- a/lib/gitlab/ci/features.rb +++ b/lib/gitlab/ci/features.rb @@ -57,7 +57,7 @@ module Gitlab end def self.ci_if_parenthesis_enabled? - ::Feature.enabled?(:ci_if_parenthesis_enabled) + ::Feature.enabled?(:ci_if_parenthesis_enabled, default_enabled: true) end def self.allow_to_create_merge_request_pipelines_in_target_project?(target_project) @@ -65,7 +65,7 @@ module Gitlab end def self.ci_plan_needs_size_limit?(project) - ::Feature.enabled?(:ci_plan_needs_size_limit, project) + ::Feature.enabled?(:ci_plan_needs_size_limit, project, default_enabled: true) end def self.job_entry_matches_all_keys? diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index f2a7da73120..f6914389637 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -604,8 +604,6 @@ module Gitlab end def action_monthly_active_users(time_period) - return {} unless Feature.enabled?(Gitlab::UsageDataCounters::TrackUniqueActions::FEATURE_FLAG) - counter = Gitlab::UsageDataCounters::TrackUniqueActions project_count = redis_usage_data do diff --git a/lib/gitlab/usage_data_counters/track_unique_actions.rb b/lib/gitlab/usage_data_counters/track_unique_actions.rb index b7948b07251..0df982572a4 100644 --- a/lib/gitlab/usage_data_counters/track_unique_actions.rb +++ b/lib/gitlab/usage_data_counters/track_unique_actions.rb @@ -4,7 +4,6 @@ module Gitlab module UsageDataCounters module TrackUniqueActions KEY_EXPIRY_LENGTH = 29.days - FEATURE_FLAG = :track_unique_actions WIKI_ACTION = :wiki_action DESIGN_ACTION = :design_action @@ -29,7 +28,6 @@ module Gitlab class << self def track_event(event_action:, event_target:, author_id:, time: Time.zone.now) return unless Gitlab::CurrentSettings.usage_ping_enabled - return unless Feature.enabled?(FEATURE_FLAG) return unless valid_target?(event_target) return unless valid_action?(event_action) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ff2627e69a5..eba71cacad1 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -6161,6 +6161,12 @@ msgstr "" msgid "Comment/Reply (quoting selected text)" msgstr "" +msgid "Commenting on files that replace or are replaced by symbolic links is currently not supported." +msgstr "" + +msgid "Commenting on symbolic links that replace or are replaced by files is currently not supported." +msgstr "" + msgid "Comments" msgstr "" diff --git a/package.json b/package.json index 5f70378f735..29d611c341d 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "@babel/preset-env": "^7.10.1", "@gitlab/at.js": "1.5.5", "@gitlab/svgs": "1.158.0", - "@gitlab/ui": "18.7.0", + "@gitlab/ui": "20.1.1", "@gitlab/visual-review-tools": "1.6.1", "@rails/actioncable": "^6.0.3-1", "@sentry/browser": "^5.10.2", diff --git a/spec/bin/feature_flag_spec.rb b/spec/bin/feature_flag_spec.rb index e0fa24fb484..f85b8f22210 100644 --- a/spec/bin/feature_flag_spec.rb +++ b/spec/bin/feature_flag_spec.rb @@ -8,7 +8,7 @@ RSpec.describe 'bin/feature-flag' do using RSpec::Parameterized::TableSyntax describe FeatureFlagCreator do - let(:argv) { %w[feature-flag-name -t development -g group::memory -i https://url] } + let(:argv) { %w[feature-flag-name -t development -g group::memory -i https://url -m http://url] } let(:options) { FeatureFlagOptionParser.parse(argv) } let(:creator) { described_class.new(options) } let(:existing_flag) { File.join('config', 'feature_flags', 'development', 'existing-feature-flag.yml') } @@ -183,15 +183,51 @@ RSpec.describe 'bin/feature-flag' do end end - describe '.rollout_issue_url' do + describe '.read_introduced_by_url' do + let(:url) { 'https://merge-request' } + + it 'reads type from $stdin' do + expect($stdin).to receive(:gets).and_return(url) + expect do + expect(described_class.read_introduced_by_url).to eq('https://merge-request') + end.to output(/can you paste the URL here/).to_stdout + end + + context 'empty URL given' do + let(:url) { '' } + + it 'skips entry' do + expect($stdin).to receive(:gets).and_return(url) + expect do + expect(described_class.read_introduced_by_url).to be_nil + end.to output(/can you paste the URL here/).to_stdout + end + end + + context 'invalid URL given' do + let(:url) { 'invalid' } + + it 'shows error message and retries' do + expect($stdin).to receive(:gets).and_return(url) + expect($stdin).to receive(:gets).and_raise('EOF') + + expect do + expect { described_class.read_introduced_by_url }.to raise_error(/EOF/) + end.to output(/can you paste the URL here/).to_stdout + .and output(/URL needs to start with/).to_stderr + end + end + end + + describe '.read_rollout_issue_url' do let(:options) { OpenStruct.new(name: 'foo', type: :development) } let(:url) { 'https://issue' } it 'reads type from $stdin' do expect($stdin).to receive(:gets).and_return(url) expect do - expect(described_class.read_issue_url(options)).to eq('https://issue') - end.to output(/Paste URL here/).to_stdout + expect(described_class.read_rollout_issue_url(options)).to eq('https://issue') + end.to output(/Paste URL of `rollout issue` here/).to_stdout end context 'invalid URL given' do @@ -202,8 +238,8 @@ RSpec.describe 'bin/feature-flag' do expect($stdin).to receive(:gets).and_raise('EOF') expect do - expect { described_class.read_issue_url(options) }.to raise_error(/EOF/) - end.to output(/Paste URL here/).to_stdout + expect { described_class.read_rollout_issue_url(options) }.to raise_error(/EOF/) + end.to output(/Paste URL of `rollout issue` here/).to_stdout .and output(/URL needs to start/).to_stderr end end diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb index 59588978a8e..c585d7f6194 100644 --- a/spec/features/issues/filtered_search/visual_tokens_spec.rb +++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb @@ -53,7 +53,7 @@ RSpec.describe 'Visual tokens', :js do end it 'ends editing mode when document is clicked' do - find('#content-body').click + find('.js-navbar').click expect_filtered_search_input_empty expect(page).to have_css('#js-dropdown-author', visible: false) @@ -142,7 +142,7 @@ RSpec.describe 'Visual tokens', :js do it 'does not tokenize incomplete token' do filtered_search.send_keys('author:=') - find('body').click + find('.js-navbar').click token = page.all('.tokens-container .js-visual-token')[1] expect_filtered_search_input_empty diff --git a/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb b/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb index 8070fee5804..29a27992a0d 100644 --- a/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb +++ b/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb @@ -5,9 +5,9 @@ require 'spec_helper' RSpec.describe 'User uploads new design', :js do include DesignManagementTestHelpers - let_it_be(:project) { create(:project_empty_repo, :public) } - let_it_be(:user) { project.owner } - let_it_be(:issue) { create(:issue, project: project) } + let(:project) { create(:project_empty_repo, :public) } + let(:user) { project.owner } + let(:issue) { create(:issue, project: project) } before do sign_in(user) @@ -28,7 +28,7 @@ RSpec.describe 'User uploads new design', :js do let(:feature_enabled) { true } it 'uploads designs' do - attach_file(:design_file, logo_fixture, make_visible: true) + upload_design(logo_fixture, count: 1) expect(page).to have_selector('.js-design-list-item', count: 1) @@ -36,9 +36,12 @@ RSpec.describe 'User uploads new design', :js do expect(page).to have_content('dk.png') end - attach_file(:design_file, gif_fixture, make_visible: true) + upload_design(gif_fixture, count: 2) + # Known bug in the legacy implementation: new designs are inserted + # in the beginning on the frontend. expect(page).to have_selector('.js-design-list-item', count: 2) + expect(page.all('.js-design-list-item').map(&:text)).to eq(['banana_sample.gif', 'dk.png']) end end @@ -61,8 +64,8 @@ RSpec.describe 'User uploads new design', :js do context "when the feature is available" do let(:feature_enabled) { true } - it 'uploads designs', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/225616' do - attach_file(:design_file, logo_fixture, make_visible: true) + it 'uploads designs' do + upload_design(logo_fixture, count: 1) expect(page).to have_selector('.js-design-list-item', count: 1) @@ -70,9 +73,10 @@ RSpec.describe 'User uploads new design', :js do expect(page).to have_content('dk.png') end - attach_file(:design_file, gif_fixture, make_visible: true) + upload_design(gif_fixture, count: 2) expect(page).to have_selector('.js-design-list-item', count: 2) + expect(page.all('.js-design-list-item').map(&:text)).to eq(['dk.png', 'banana_sample.gif']) end end @@ -92,4 +96,12 @@ RSpec.describe 'User uploads new design', :js do def gif_fixture Rails.root.join('spec', 'fixtures', 'banana_sample.gif') end + + def upload_design(fixture, count:) + attach_file(:design_file, fixture, match: :first, make_visible: true) + + wait_for('designs uploaded') do + issue.reload.designs.count == count + end + end end diff --git a/spec/frontend/__mocks__/@toast-ui/vue-editor/index.js b/spec/frontend/__mocks__/@toast-ui/vue-editor/index.js index 726ed0fa030..9fee8e18d26 100644 --- a/spec/frontend/__mocks__/@toast-ui/vue-editor/index.js +++ b/spec/frontend/__mocks__/@toast-ui/vue-editor/index.js @@ -17,6 +17,17 @@ export const Editor = { type: String, }, }, + created() { + const mockEditorApi = { + eventManager: { + addEventType: jest.fn(), + listen: jest.fn(), + removeEventHandler: jest.fn(), + }, + }; + + this.$emit('load', mockEditorApi); + }, render(h) { return h('div'); }, diff --git a/spec/frontend/alert_settings/__snapshots__/alert_settings_form_spec.js.snap b/spec/frontend/alert_settings/__snapshots__/alert_settings_form_spec.js.snap index f6036dcbc1d..f271aeb5d95 100644 --- a/spec/frontend/alert_settings/__snapshots__/alert_settings_form_spec.js.snap +++ b/spec/frontend/alert_settings/__snapshots__/alert_settings_form_spec.js.snap @@ -27,7 +27,7 @@ exports[`AlertsSettingsForm with default values renders the initial template 1`] <gl-form-group-stub label=\\"Authorization key\\" label-for=\\"authorization-key\\" label-class=\\"label-bold\\"> <gl-form-input-group-stub value=\\"abcedfg123\\" predefinedoptions=\\"[object Object]\\" id=\\"authorization-key\\" readonly=\\"\\" class=\\"gl-mb-2\\"></gl-form-input-group-stub> <div class=\\"gl-display-flex gl-justify-content-end\\"> - <gl-button-stub category=\\"tertiary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" disabled=\\"true\\" class=\\"gl-mt-3\\" role=\\"button\\" tabindex=\\"0\\">Reset key</gl-button-stub> + <gl-button-stub category=\\"primary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" disabled=\\"true\\" class=\\"gl-mt-3\\" role=\\"button\\" tabindex=\\"0\\">Reset key</gl-button-stub> </div> <gl-modal-stub modalid=\\"authKeyModal\\" titletag=\\"h4\\" modalclass=\\"\\" size=\\"md\\" title=\\"Reset key\\" ok-title=\\"Reset key\\" ok-variant=\\"danger\\"> Resetting the authorization key for this project will require updating the authorization key in every alert source it is enabled in. @@ -37,7 +37,7 @@ exports[`AlertsSettingsForm with default values renders the initial template 1`] <gl-form-textarea-stub noresize=\\"true\\" id=\\"alert-json\\" disabled=\\"true\\" state=\\"true\\" placeholder=\\"Enter test alert JSON....\\" rows=\\"6\\" max-rows=\\"10\\"></gl-form-textarea-stub> </gl-form-group-stub> <div class=\\"gl-display-flex gl-justify-content-end\\"> - <gl-button-stub category=\\"tertiary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" disabled=\\"true\\">Test alert payload</gl-button-stub> + <gl-button-stub category=\\"primary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" disabled=\\"true\\">Test alert payload</gl-button-stub> </div> <div class=\\"footer-block row-content-block gl-display-flex gl-justify-content-space-between\\"> <gl-button-stub category=\\"primary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" disabled=\\"true\\"> diff --git a/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap b/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap index 161c2bade05..745a163951a 100644 --- a/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap +++ b/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap @@ -56,7 +56,7 @@ exports[`Code navigation popover component renders popover 1`] = ` class="popover-body border-top" > <gl-button-stub - category="tertiary" + category="primary" class="w-100" data-testid="go-to-definition-btn" href="http://gitlab.com/test.js" diff --git a/spec/frontend/design_management/components/toolbar/__snapshots__/index_spec.js.snap b/spec/frontend/design_management/components/toolbar/__snapshots__/index_spec.js.snap index aa914a8dff0..1900c64cd47 100644 --- a/spec/frontend/design_management/components/toolbar/__snapshots__/index_spec.js.snap +++ b/spec/frontend/design_management/components/toolbar/__snapshots__/index_spec.js.snap @@ -44,7 +44,7 @@ exports[`Design management toolbar component renders design and updated data 1`] /> <gl-button-stub - category="tertiary" + category="primary" href="/-/designs/306/7f747adcd4693afadbe968d7ba7d983349b9012d" icon="download" size="medium" diff --git a/spec/frontend/design_management/components/upload/__snapshots__/button_spec.js.snap b/spec/frontend/design_management/components/upload/__snapshots__/button_spec.js.snap index c4112b46402..3d7939df28e 100644 --- a/spec/frontend/design_management/components/upload/__snapshots__/button_spec.js.snap +++ b/spec/frontend/design_management/components/upload/__snapshots__/button_spec.js.snap @@ -5,7 +5,7 @@ exports[`Design management upload button component renders inverted upload desig isinverted="true" > <gl-button-stub - category="tertiary" + category="primary" icon="" size="small" title="Adding a design with the same filename replaces the file in a new version." @@ -30,7 +30,7 @@ exports[`Design management upload button component renders inverted upload desig exports[`Design management upload button component renders loading icon 1`] = ` <div> <gl-button-stub - category="tertiary" + category="primary" disabled="true" icon="" size="small" @@ -62,7 +62,7 @@ exports[`Design management upload button component renders loading icon 1`] = ` exports[`Design management upload button component renders upload design button 1`] = ` <div> <gl-button-stub - category="tertiary" + category="primary" icon="" size="small" title="Adding a design with the same filename replaces the file in a new version." diff --git a/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap b/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap index c3fcede5576..3881b2d7679 100644 --- a/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap +++ b/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap @@ -110,7 +110,7 @@ exports[`Design management index page designs renders designs list and header wi class="qa-selector-toolbar gl-display-flex gl-align-items-center" > <gl-button-stub - category="tertiary" + category="primary" class="gl-mr-3 js-select-all" icon="" size="small" diff --git a/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap b/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap index a7b754ba50d..823294efc38 100644 --- a/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap +++ b/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap @@ -65,7 +65,7 @@ exports[`Design management design index page renders design index 1`] = ` /> <gl-button-stub - category="tertiary" + category="primary" class="link-inherit-color gl-text-body gl-text-decoration-none gl-font-weight-bold gl-mb-4" data-testid="resolved-comments" icon="chevron-right" diff --git a/spec/frontend/design_management_legacy/pages/design/__snapshots__/index_spec.js.snap b/spec/frontend/design_management_legacy/pages/design/__snapshots__/index_spec.js.snap index c3417744524..dc5baf37fc6 100644 --- a/spec/frontend/design_management_legacy/pages/design/__snapshots__/index_spec.js.snap +++ b/spec/frontend/design_management_legacy/pages/design/__snapshots__/index_spec.js.snap @@ -65,7 +65,7 @@ exports[`Design management design index page renders design index 1`] = ` /> <gl-button-stub - category="tertiary" + category="primary" class="link-inherit-color gl-text-body gl-text-decoration-none gl-font-weight-bold gl-mb-4" data-testid="resolved-comments" icon="chevron-right" diff --git a/spec/frontend/diffs/components/diff_table_cell_spec.js b/spec/frontend/diffs/components/diff_table_cell_spec.js index 24daef62ee2..02f5c27eecb 100644 --- a/spec/frontend/diffs/components/diff_table_cell_spec.js +++ b/spec/frontend/diffs/components/diff_table_cell_spec.js @@ -18,6 +18,12 @@ const TEST_LINE_CODE = 'LC_42'; const TEST_FILE_HASH = diffFileMockData.file_hash; describe('DiffTableCell', () => { + const symlinkishFileTooltip = + 'Commenting on symbolic links that replace or are replaced by files is currently not supported.'; + const realishFileTooltip = + 'Commenting on files that replace or are replaced by symbolic links is currently not supported.'; + const otherFileTooltip = 'Add a comment to this line'; + let wrapper; let line; let store; @@ -67,6 +73,7 @@ describe('DiffTableCell', () => { const findTd = () => wrapper.find({ ref: 'td' }); const findNoteButton = () => wrapper.find({ ref: 'addDiffNoteButton' }); const findLineNumber = () => wrapper.find({ ref: 'lineNumberRef' }); + const findTooltip = () => wrapper.find({ ref: 'addNoteTooltip' }); const findAvatars = () => wrapper.find(DiffGutterAvatars); describe('td', () => { @@ -134,6 +141,53 @@ describe('DiffTableCell', () => { }); }, ); + + it.each` + disabled | commentsDisabled + ${'disabled'} | ${true} + ${undefined} | ${false} + `( + 'has attribute disabled=$disabled when the outer component has prop commentsDisabled=$commentsDisabled', + ({ disabled, commentsDisabled }) => { + line.commentsDisabled = commentsDisabled; + + createComponent({ + showCommentButton: true, + isHover: true, + }); + + wrapper.setData({ isCommentButtonRendered: true }); + + return wrapper.vm.$nextTick().then(() => { + expect(findNoteButton().attributes('disabled')).toBe(disabled); + }); + }, + ); + + it.each` + tooltip | commentsDisabled + ${symlinkishFileTooltip} | ${{ wasSymbolic: true }} + ${symlinkishFileTooltip} | ${{ isSymbolic: true }} + ${realishFileTooltip} | ${{ wasReal: true }} + ${realishFileTooltip} | ${{ isReal: true }} + ${otherFileTooltip} | ${false} + `( + 'has the correct tooltip when commentsDisabled=$commentsDisabled', + ({ tooltip, commentsDisabled }) => { + line.commentsDisabled = commentsDisabled; + + createComponent({ + showCommentButton: true, + isHover: true, + }); + + wrapper.setData({ isCommentButtonRendered: true }); + + return wrapper.vm.$nextTick().then(() => { + expect(findTooltip().attributes('title')).toBe(tooltip); + }); + }, + ); }); describe('line number', () => { diff --git a/spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js b/spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js index e59ee925cc7..6a00065c9fe 100644 --- a/spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js +++ b/spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js @@ -280,8 +280,8 @@ describe('Filtered Search Visual Tokens', () => { ); }); - it('contains fa-close icon', () => { - expect(tokenElement.querySelector('.remove-token .fa-close')).toEqual(expect.anything()); + it('contains close icon', () => { + expect(tokenElement.querySelector('.remove-token .close-icon')).toEqual(expect.anything()); }); }); }); diff --git a/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap b/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap index 8a61f5565c6..0e16b726c4b 100644 --- a/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap +++ b/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap @@ -17,7 +17,7 @@ exports[`grafana integration component default state to match the default snapsh </h3> <gl-button-stub - category="tertiary" + category="primary" class="js-settings-toggle" icon="" size="medium" diff --git a/spec/frontend/helpers/filtered_search_spec_helper.js b/spec/frontend/helpers/filtered_search_spec_helper.js index ceb7982bbc3..ecf10694a16 100644 --- a/spec/frontend/helpers/filtered_search_spec_helper.js +++ b/spec/frontend/helpers/filtered_search_spec_helper.js @@ -15,7 +15,7 @@ export default class FilteredSearchSpecHelper { <div class="value-container"> <div class="value">${value}</div> <div class="remove-token" role="button"> - <i class="fa fa-close"></i> + <svg class="s16 close-icon"></svg> </div> </div> </div> diff --git a/spec/frontend/incidents_settings/components/__snapshots__/alerts_form_spec.js.snap b/spec/frontend/incidents_settings/components/__snapshots__/alerts_form_spec.js.snap index ef61a835918..f3f610e4bb7 100644 --- a/spec/frontend/incidents_settings/components/__snapshots__/alerts_form_spec.js.snap +++ b/spec/frontend/incidents_settings/components/__snapshots__/alerts_form_spec.js.snap @@ -85,7 +85,7 @@ exports[`Alert integration settings form default state should match the default class="gl-display-flex gl-justify-content-end" > <gl-button-stub - category="tertiary" + category="primary" class="js-no-auto-disable" data-qa-selector="save_changes_button" icon="" diff --git a/spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap b/spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap index 31978f8da64..3ad4c13382d 100644 --- a/spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap +++ b/spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap @@ -18,7 +18,7 @@ exports[`IncidentsSettingTabs should render the component 1`] = ` </h4> <gl-button-stub - category="tertiary" + category="primary" class="js-settings-toggle" icon="" size="medium" diff --git a/spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap b/spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap index c0fee56c9d8..78bb238fcb6 100644 --- a/spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap +++ b/spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap @@ -46,7 +46,7 @@ exports[`Alert integration settings form should match the default snapshot 1`] = class="gl-display-flex gl-justify-content-end" > <gl-button-stub - category="tertiary" + category="primary" class="gl-mt-3" data-testid="webhook-reset-btn" icon="" @@ -80,7 +80,7 @@ exports[`Alert integration settings form should match the default snapshot 1`] = class="gl-display-flex gl-justify-content-end" > <gl-button-stub - category="tertiary" + category="primary" class="js-no-auto-disable" icon="" size="medium" diff --git a/spec/frontend/pages/admin/users/components/__snapshots__/delete_user_modal_spec.js.snap b/spec/frontend/pages/admin/users/components/__snapshots__/delete_user_modal_spec.js.snap index 4148c263d28..fc37a545511 100644 --- a/spec/frontend/pages/admin/users/components/__snapshots__/delete_user_modal_spec.js.snap +++ b/spec/frontend/pages/admin/users/components/__snapshots__/delete_user_modal_spec.js.snap @@ -38,7 +38,7 @@ exports[`User Operation confirmation modal renders modal with form included 1`] /> </form> <gl-button-stub - category="tertiary" + category="primary" icon="" size="medium" variant="default" diff --git a/spec/frontend/projects/components/shared/__snapshots__/delete_button_spec.js.snap b/spec/frontend/projects/components/shared/__snapshots__/delete_button_spec.js.snap index 51fb9a0dede..a43acc8c002 100644 --- a/spec/frontend/projects/components/shared/__snapshots__/delete_button_spec.js.snap +++ b/spec/frontend/projects/components/shared/__snapshots__/delete_button_spec.js.snap @@ -82,7 +82,7 @@ exports[`Project remove modal intialized matches the snapshot 1`] = ` <template> <gl-button-stub - category="tertiary" + category="primary" class="js-modal-action-cancel" icon="" size="medium" @@ -96,7 +96,7 @@ exports[`Project remove modal intialized matches the snapshot 1`] = ` <!----> <gl-button-stub - category="tertiary" + category="primary" class="js-modal-action-primary" disabled="true" icon="" diff --git a/spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap b/spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap index e5e436278a3..22689080063 100644 --- a/spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap +++ b/spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap @@ -11,7 +11,7 @@ exports[`EmptyStateComponent should render content 1`] = ` <p>In order to start using functions as a service, you must first install Knative on your Kubernetes cluster. <gl-link-stub href=\\"/help\\">More information</gl-link-stub> </p> <div> - <gl-button-stub category=\\"tertiary\\" variant=\\"success\\" size=\\"medium\\" icon=\\"\\" href=\\"/clusters\\">Install Knative</gl-button-stub> + <gl-button-stub category=\\"primary\\" variant=\\"success\\" size=\\"medium\\" icon=\\"\\" href=\\"/clusters\\">Install Knative</gl-button-stub> <!----> </div> </div> diff --git a/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap index 71cfa394957..e84eb7789d3 100644 --- a/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap +++ b/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap @@ -39,7 +39,7 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = ` tag="div" > <gl-button-stub - category="tertiary" + category="primary" class="d-inline-flex" data-clipboard-text="ssh://foo.bar" data-qa-selector="copy_ssh_url_button" @@ -80,7 +80,7 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = ` tag="div" > <gl-button-stub - category="tertiary" + category="primary" class="d-inline-flex" data-clipboard-text="http://foo.bar" data-qa-selector="copy_http_url_button" diff --git a/spec/frontend/vue_shared/components/rich_content_editor/editor_service_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/editor_service_spec.js index 78f27c9948b..16f60b5ff21 100644 --- a/spec/frontend/vue_shared/components/rich_content_editor/editor_service_spec.js +++ b/spec/frontend/vue_shared/components/rich_content_editor/editor_service_spec.js @@ -5,10 +5,13 @@ import { registerHTMLToMarkdownRenderer, addImage, getMarkdown, + getEditorOptions, } from '~/vue_shared/components/rich_content_editor/services/editor_service'; import buildHTMLToMarkdownRenderer from '~/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer'; +import buildCustomRenderer from '~/vue_shared/components/rich_content_editor/services/build_custom_renderer'; jest.mock('~/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer'); +jest.mock('~/vue_shared/components/rich_content_editor/services/build_custom_renderer'); describe('Editor Service', () => { let mockInstance; @@ -120,4 +123,25 @@ describe('Editor Service', () => { expect(mockInstance.toMarkOptions.renderer).toBe(extendedRenderer); }); }); + + describe('getEditorOptions', () => { + const externalOptions = { + customRenderers: {}, + }; + const renderer = {}; + + beforeEach(() => { + buildCustomRenderer.mockReturnValueOnce(renderer); + }); + + it('generates a configuration object with a custom HTML renderer and toolbarItems', () => { + expect(getEditorOptions()).toHaveProp('customHTMLRenderer', renderer); + expect(getEditorOptions()).toHaveProp('toolbarItems'); + }); + + it('passes external renderers to the buildCustomRenderers function', () => { + getEditorOptions(externalOptions); + expect(buildCustomRenderer).toHaveBeenCalledWith(externalOptions.customRenderers); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js index b6ff6aa767c..3d54db7fe5c 100644 --- a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js +++ b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js @@ -2,7 +2,6 @@ import { shallowMount } from '@vue/test-utils'; import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue'; import AddImageModal from '~/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue'; import { - EDITOR_OPTIONS, EDITOR_TYPES, EDITOR_HEIGHT, EDITOR_PREVIEW_STYLE, @@ -14,6 +13,7 @@ import { removeCustomEventListener, addImage, registerHTMLToMarkdownRenderer, + getEditorOptions, } from '~/vue_shared/components/rich_content_editor/services/editor_service'; jest.mock('~/vue_shared/components/rich_content_editor/services/editor_service', () => ({ @@ -22,6 +22,7 @@ jest.mock('~/vue_shared/components/rich_content_editor/services/editor_service', removeCustomEventListener: jest.fn(), addImage: jest.fn(), registerHTMLToMarkdownRenderer: jest.fn(), + getEditorOptions: jest.fn(), })); describe('Rich Content Editor', () => { @@ -32,13 +33,25 @@ describe('Rich Content Editor', () => { const findEditor = () => wrapper.find({ ref: 'editor' }); const findAddImageModal = () => wrapper.find(AddImageModal); - beforeEach(() => { + const buildWrapper = () => { wrapper = shallowMount(RichContentEditor, { propsData: { content, imageRoot }, }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; }); describe('when content is loaded', () => { + const editorOptions = {}; + + beforeEach(() => { + getEditorOptions.mockReturnValueOnce(editorOptions); + buildWrapper(); + }); + it('renders an editor', () => { expect(findEditor().exists()).toBe(true); }); @@ -47,8 +60,8 @@ describe('Rich Content Editor', () => { expect(findEditor().props().initialValue).toBe(content); }); - it('provides the correct editor options', () => { - expect(findEditor().props().options).toEqual(EDITOR_OPTIONS); + it('provides options generated by the getEditorOptions service', () => { + expect(findEditor().props().options).toBe(editorOptions); }); it('has the correct preview style', () => { @@ -65,6 +78,10 @@ describe('Rich Content Editor', () => { }); describe('when content is changed', () => { + beforeEach(() => { + buildWrapper(); + }); + it('emits an input event with the changed content', () => { const changedMarkdown = '## Changed Markdown'; const getMarkdownMock = jest.fn().mockReturnValueOnce(changedMarkdown); @@ -77,6 +94,10 @@ describe('Rich Content Editor', () => { }); describe('when content is reset', () => { + beforeEach(() => { + buildWrapper(); + }); + it('should reset the content via setMarkdown', () => { const newContent = 'Just the body content excluding the front matter for example'; const mockInstance = { invoke: jest.fn() }; @@ -89,35 +110,33 @@ describe('Rich Content Editor', () => { }); describe('when editor is loaded', () => { - let mockEditorApi; - beforeEach(() => { - mockEditorApi = { eventManager: { addEventType: jest.fn(), listen: jest.fn() } }; - findEditor().vm.$emit('load', mockEditorApi); + buildWrapper(); }); it('adds the CUSTOM_EVENTS.openAddImageModal custom event listener', () => { expect(addCustomEventListener).toHaveBeenCalledWith( - mockEditorApi, + wrapper.vm.editorApi, CUSTOM_EVENTS.openAddImageModal, wrapper.vm.onOpenAddImageModal, ); }); it('registers HTML to markdown renderer', () => { - expect(registerHTMLToMarkdownRenderer).toHaveBeenCalledWith(mockEditorApi); + expect(registerHTMLToMarkdownRenderer).toHaveBeenCalledWith(wrapper.vm.editorApi); }); }); describe('when editor is destroyed', () => { - it('removes the CUSTOM_EVENTS.openAddImageModal custom event listener', () => { - const mockEditorApi = { eventManager: { removeEventHandler: jest.fn() } }; + beforeEach(() => { + buildWrapper(); + }); - wrapper.vm.editorApi = mockEditorApi; + it('removes the CUSTOM_EVENTS.openAddImageModal custom event listener', () => { wrapper.vm.$destroy(); expect(removeCustomEventListener).toHaveBeenCalledWith( - mockEditorApi, + wrapper.vm.editorApi, CUSTOM_EVENTS.openAddImageModal, wrapper.vm.onOpenAddImageModal, ); @@ -125,6 +144,10 @@ describe('Rich Content Editor', () => { }); describe('add image modal', () => { + beforeEach(() => { + buildWrapper(); + }); + it('renders an addImageModal component', () => { expect(findAddImageModal().exists()).toBe(true); }); diff --git a/spec/lib/gitlab/background_migration/backfill_designs_relative_position_spec.rb b/spec/lib/gitlab/background_migration/backfill_designs_relative_position_spec.rb deleted file mode 100644 index 1b723502663..00000000000 --- a/spec/lib/gitlab/background_migration/backfill_designs_relative_position_spec.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::BackgroundMigration::BackfillDesignsRelativePosition do - let(:namespace) { table(:namespaces).create!(name: 'gitlab', path: 'gitlab') } - let(:project) { table(:projects).create!(namespace_id: namespace.id) } - let(:issues) { table(:issues) } - let(:designs) { table(:design_management_designs) } - - before do - issues.create!(id: 1, project_id: project.id) - issues.create!(id: 2, project_id: project.id) - issues.create!(id: 3, project_id: project.id) - issues.create!(id: 4, project_id: project.id) - - designs.create!(id: 1, issue_id: 1, project_id: project.id, filename: 'design1.jpg') - designs.create!(id: 2, issue_id: 1, project_id: project.id, filename: 'design2.jpg') - designs.create!(id: 3, issue_id: 2, project_id: project.id, filename: 'design3.jpg') - designs.create!(id: 4, issue_id: 2, project_id: project.id, filename: 'design4.jpg') - designs.create!(id: 5, issue_id: 3, project_id: project.id, filename: 'design5.jpg') - end - - describe '#perform' do - it 'backfills the position for the designs in each issue' do - expect(described_class::Design).to receive(:move_nulls_to_start).with( - a_collection_containing_exactly( - an_object_having_attributes(id: 1, issue_id: 1), - an_object_having_attributes(id: 2, issue_id: 1) - ) - ).ordered.and_call_original - - expect(described_class::Design).to receive(:move_nulls_to_start).with( - a_collection_containing_exactly( - an_object_having_attributes(id: 3, issue_id: 2), - an_object_having_attributes(id: 4, issue_id: 2) - ) - ).ordered.and_call_original - - # We only expect calls to `move_nulls_to_start` with issues 1 and 2: - # - Issue 3 should be skipped because we're not passing its ID - # - Issue 4 should be skipped because it doesn't have any designs - # - Issue 0 should be skipped because it doesn't exist - subject.perform([1, 2, 4, 0]) - - expect(designs.find(1).relative_position).to be < designs.find(2).relative_position - expect(designs.find(3).relative_position).to be < designs.find(4).relative_position - expect(designs.find(5).relative_position).to be_nil - end - end -end diff --git a/spec/lib/gitlab/usage_data_counters/track_unique_actions_spec.rb b/spec/lib/gitlab/usage_data_counters/track_unique_actions_spec.rb index 6db77f19877..bd348666729 100644 --- a/spec/lib/gitlab/usage_data_counters/track_unique_actions_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/track_unique_actions_spec.rb @@ -17,10 +17,9 @@ RSpec.describe Gitlab::UsageDataCounters::TrackUniqueActions, :clean_gitlab_redi context 'tracking an event' do context 'when tracking successfully' do - context 'when the feature flag and the application setting is enabled' do + context 'when the application setting is enabled' do context 'when the target and the action is valid' do before do - stub_feature_flags(described_class::FEATURE_FLAG => true) stub_application_setting(usage_ping_enabled: true) end @@ -59,17 +58,15 @@ RSpec.describe Gitlab::UsageDataCounters::TrackUniqueActions, :clean_gitlab_redi context 'when tracking unsuccessfully' do using RSpec::Parameterized::TableSyntax - where(:feature_flag, :application_setting, :target, :action) do - true | true | Project | :invalid_action - false | true | Project | :pushed - true | false | Project | :pushed - true | true | :invalid_target | :pushed + where(:application_setting, :target, :action) do + true | Project | :invalid_action + false | Project | :pushed + true | :invalid_target | :pushed end with_them do before do stub_application_setting(usage_ping_enabled: application_setting) - stub_feature_flags(described_class::FEATURE_FLAG => feature_flag) end it 'returns the expected values' do diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 2fb30b3228a..e3da0163e8d 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -912,45 +912,29 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do let(:time) { Time.zone.now } before do - stub_feature_flags(Gitlab::UsageDataCounters::TrackUniqueActions::FEATURE_FLAG => feature_flag) - end - - context 'when the feature flag is enabled' do - let(:feature_flag) { true } - - before do - counter = Gitlab::UsageDataCounters::TrackUniqueActions - project = Event::TARGET_TYPES[:project] - wiki = Event::TARGET_TYPES[:wiki] - design = Event::TARGET_TYPES[:design] - - counter.track_event(event_action: :pushed, event_target: project, author_id: 1) - counter.track_event(event_action: :pushed, event_target: project, author_id: 1) - counter.track_event(event_action: :pushed, event_target: project, author_id: 2) - counter.track_event(event_action: :pushed, event_target: project, author_id: 3) - counter.track_event(event_action: :pushed, event_target: project, author_id: 4, time: time - 3.days) - counter.track_event(event_action: :created, event_target: project, author_id: 5, time: time - 3.days) - counter.track_event(event_action: :created, event_target: wiki, author_id: 3) - counter.track_event(event_action: :created, event_target: design, author_id: 3) - end - - it 'returns the distinct count of user actions within the specified time period' do - expect(described_class.action_monthly_active_users(time_period)).to eq( - { - action_monthly_active_users_design_management: 1, - action_monthly_active_users_project_repo: 3, - action_monthly_active_users_wiki_repo: 1 - } - ) - end - end - - context 'when the feature flag is disabled' do - let(:feature_flag) { false } - - it 'returns an empty hash' do - expect(described_class.action_monthly_active_users(time_period)).to eq({}) - end + counter = Gitlab::UsageDataCounters::TrackUniqueActions + project = Event::TARGET_TYPES[:project] + wiki = Event::TARGET_TYPES[:wiki] + design = Event::TARGET_TYPES[:design] + + counter.track_event(event_action: :pushed, event_target: project, author_id: 1) + counter.track_event(event_action: :pushed, event_target: project, author_id: 1) + counter.track_event(event_action: :pushed, event_target: project, author_id: 2) + counter.track_event(event_action: :pushed, event_target: project, author_id: 3) + counter.track_event(event_action: :pushed, event_target: project, author_id: 4, time: time - 3.days) + counter.track_event(event_action: :created, event_target: project, author_id: 5, time: time - 3.days) + counter.track_event(event_action: :created, event_target: wiki, author_id: 3) + counter.track_event(event_action: :created, event_target: design, author_id: 3) + end + + it 'returns the distinct count of user actions within the specified time period' do + expect(described_class.action_monthly_active_users(time_period)).to eq( + { + action_monthly_active_users_design_management: 1, + action_monthly_active_users_project_repo: 3, + action_monthly_active_users_wiki_repo: 1 + } + ) end end diff --git a/spec/migrations/backfill_designs_relative_position_spec.rb b/spec/migrations/backfill_designs_relative_position_spec.rb deleted file mode 100644 index 878d3385bb3..00000000000 --- a/spec/migrations/backfill_designs_relative_position_spec.rb +++ /dev/null @@ -1,41 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require Rails.root.join('db', 'post_migrate', '20200724130639_backfill_designs_relative_position.rb') - -RSpec.describe BackfillDesignsRelativePosition do - let(:namespace) { table(:namespaces).create!(name: 'gitlab', path: 'gitlab') } - let(:project) { table(:projects).create!(namespace_id: namespace.id) } - let(:issues) { table(:issues) } - let(:designs) { table(:design_management_designs) } - - before do - issues.create!(id: 1, project_id: project.id) - issues.create!(id: 2, project_id: project.id) - issues.create!(id: 3, project_id: project.id) - issues.create!(id: 4, project_id: project.id) - - designs.create!(issue_id: 1, project_id: project.id, filename: 'design1.jpg') - designs.create!(issue_id: 2, project_id: project.id, filename: 'design2.jpg') - designs.create!(issue_id: 4, project_id: project.id, filename: 'design3.jpg') - - stub_const("#{described_class.name}::BATCH_SIZE", 2) - end - - it 'correctly schedules background migrations' do - Sidekiq::Testing.fake! do - Timecop.freeze do - migrate! - - expect(described_class::MIGRATION) - .to be_scheduled_delayed_migration(2.minutes, [1, 2]) - - expect(described_class::MIGRATION) - .to be_scheduled_delayed_migration(4.minutes, [4]) - - # Issue 3 should be skipped because it doesn't have any designs - expect(BackgroundMigrationWorker.jobs.size).to eq(2) - end - end - end -end diff --git a/spec/models/design_management/design_collection_spec.rb b/spec/models/design_management/design_collection_spec.rb index a48dc457c38..de766d5ce09 100644 --- a/spec/models/design_management/design_collection_spec.rb +++ b/spec/models/design_management/design_collection_spec.rb @@ -37,9 +37,11 @@ RSpec.describe DesignManagement::DesignCollection do it 'inserts the design after any existing designs' do design1 = collection.find_or_create_design!(filename: 'design1.jpg') + design1.update!(relative_position: 100) + design2 = collection.find_or_create_design!(filename: 'design2.jpg') - expect(design1.relative_position).to be < design2.relative_position + expect(collection.designs.ordered(issue.project)).to eq([design1, design2]) end end diff --git a/spec/presenters/packages/detail/package_presenter_spec.rb b/spec/presenters/packages/detail/package_presenter_spec.rb index a29942c3dac..3a13aca6c7a 100644 --- a/spec/presenters/packages/detail/package_presenter_spec.rb +++ b/spec/presenters/packages/detail/package_presenter_spec.rb @@ -74,6 +74,15 @@ RSpec.describe ::Packages::Detail::PackagePresenter do end end + context 'with conan metadata' do + let(:package) { create(:conan_package, project: project) } + let(:expected_package_details) { super().merge(conan_metadatum: package.conan_metadatum) } + + it 'returns conan_metadatum' do + expect(presenter.detail_view).to eq expected_package_details + end + end + context 'with composer metadata' do let(:package) { create(:composer_package, :with_metadatum, sha: '123', project: project) } let(:expected_package_details) { super().merge(composer_metadatum: package.composer_metadatum) } diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb index edd585cb4b6..a91519a710f 100644 --- a/spec/services/event_create_service_spec.rb +++ b/spec/services/event_create_service_spec.rb @@ -202,7 +202,6 @@ RSpec.describe EventCreateService do end it 'records the event in the event counter' do - stub_feature_flags(Gitlab::UsageDataCounters::TrackUniqueActions::FEATURE_FLAG => true) counter_class = Gitlab::UsageDataCounters::TrackUniqueActions tracking_params = { event_action: counter_class::WIKI_ACTION, date_from: Date.yesterday, date_to: Date.today } @@ -244,7 +243,6 @@ RSpec.describe EventCreateService do it_behaves_like 'service for creating a push event', PushEventPayloadService it 'records the event in the event counter' do - stub_feature_flags(Gitlab::UsageDataCounters::TrackUniqueActions::FEATURE_FLAG => true) counter_class = Gitlab::UsageDataCounters::TrackUniqueActions tracking_params = { event_action: counter_class::PUSH_ACTION, date_from: Date.yesterday, date_to: Date.today } @@ -268,7 +266,6 @@ RSpec.describe EventCreateService do it_behaves_like 'service for creating a push event', BulkPushEventPayloadService it 'records the event in the event counter' do - stub_feature_flags(Gitlab::UsageDataCounters::TrackUniqueActions::FEATURE_FLAG => true) counter_class = Gitlab::UsageDataCounters::TrackUniqueActions tracking_params = { event_action: counter_class::PUSH_ACTION, date_from: Date.yesterday, date_to: Date.today } @@ -323,7 +320,6 @@ RSpec.describe EventCreateService do end it 'records the event in the event counter' do - stub_feature_flags(Gitlab::UsageDataCounters::TrackUniqueActions::FEATURE_FLAG => true) counter_class = Gitlab::UsageDataCounters::TrackUniqueActions tracking_params = { event_action: counter_class::DESIGN_ACTION, date_from: Date.yesterday, date_to: Date.today } @@ -351,7 +347,6 @@ RSpec.describe EventCreateService do end it 'records the event in the event counter' do - stub_feature_flags(Gitlab::UsageDataCounters::TrackUniqueActions::FEATURE_FLAG => true) counter_class = Gitlab::UsageDataCounters::TrackUniqueActions tracking_params = { event_action: counter_class::DESIGN_ACTION, date_from: Date.yesterday, date_to: Date.today } diff --git a/spec/services/jira_import/cloud_users_mapper_service_spec.rb b/spec/services/jira_import/cloud_users_mapper_service_spec.rb new file mode 100644 index 00000000000..591f80f3efc --- /dev/null +++ b/spec/services/jira_import/cloud_users_mapper_service_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe JiraImport::CloudUsersMapperService do + let(:start_at) { 7 } + let(:url) { "/rest/api/2/users?maxResults=50&startAt=#{start_at}" } + let(:jira_users) do + [ + { 'accountId' => 'abcd', 'displayName' => 'user1' }, + { 'accountId' => 'efg' }, + { 'accountId' => 'hij', 'displayName' => 'user3', 'emailAddress' => 'user3@example.com' } + ] + end + + describe '#execute' do + it_behaves_like 'mapping jira users' + end +end diff --git a/spec/services/jira_import/server_users_mapper_service_spec.rb b/spec/services/jira_import/server_users_mapper_service_spec.rb new file mode 100644 index 00000000000..22cb0327cc5 --- /dev/null +++ b/spec/services/jira_import/server_users_mapper_service_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe JiraImport::ServerUsersMapperService do + let(:start_at) { 7 } + let(:url) { "/rest/api/2/user/search?username=''&maxResults=50&startAt=#{start_at}" } + let(:jira_users) do + [ + { 'key' => 'abcd', 'name' => 'user1' }, + { 'key' => 'efg' }, + { 'key' => 'hij', 'name' => 'user3', 'emailAddress' => 'user3@example.com' } + ] + end + + describe '#execute' do + it_behaves_like 'mapping jira users' + end +end diff --git a/spec/services/jira_import/users_importer_spec.rb b/spec/services/jira_import/users_importer_spec.rb index 64cdc70f612..efb303dab9f 100644 --- a/spec/services/jira_import/users_importer_spec.rb +++ b/spec/services/jira_import/users_importer_spec.rb @@ -14,6 +14,27 @@ RSpec.describe JiraImport::UsersImporter do subject { importer.execute } describe '#execute' do + let(:mapped_users) do + [ + { + jira_account_id: 'acc1', + jira_display_name: 'user1', + jira_email: 'sample@jira.com', + gitlab_id: nil, + gitlab_username: nil, + gitlab_name: nil + }, + { + jira_account_id: 'acc2', + jira_display_name: 'user2', + jira_email: nil, + gitlab_id: nil, + gitlab_username: nil, + gitlab_name: nil + } + ] + end + before do stub_jira_service_test project.add_maintainer(user) @@ -25,53 +46,83 @@ RSpec.describe JiraImport::UsersImporter do end end - context 'when Jira import is configured correctly' do - let_it_be(:jira_service) { create(:jira_service, project: project, active: true) } - let(:client) { double } + RSpec.shared_examples 'maps jira users to gitlab users' do + context 'when Jira import is configured correctly' do + let_it_be(:jira_service) { create(:jira_service, project: project, active: true) } + let(:client) { double } - before do - expect(importer).to receive(:client).and_return(client) - end - - context 'when jira client raises an error' do - it 'returns an error response' do - expect(client).to receive(:get).and_raise(Timeout::Error) - - expect(subject.error?).to be_truthy - expect(subject.message).to include('There was an error when communicating to Jira') - end - end - - context 'when jira client returns result' do before do - allow(client).to receive(:get).with('/rest/api/2/users?maxResults=50&startAt=7') - .and_return(jira_users) + expect(importer).to receive(:client).at_least(1).and_return(client) + allow(client).to receive_message_chain(:ServerInfo, :all, :deploymentType).and_return(deployment_type) end - context 'when jira client returns an empty array' do - let(:jira_users) { [] } + context 'when jira client raises an error' do + it 'returns an error response' do + expect(client).to receive(:get).and_raise(Timeout::Error) - it 'retturns nil payload' do - expect(subject.success?).to be_truthy - expect(subject.payload).to be_nil + expect(subject.error?).to be_truthy + expect(subject.message).to include('There was an error when communicating to Jira') end end - context 'when jira client returns an results' do - let(:jira_users) { [{ 'name' => 'user1' }, { 'name' => 'user2' }] } - let(:mapped_users) { [{ jira_display_name: 'user1', gitlab_id: 5 }] } + context 'when jira client returns result' do + context 'when jira client returns an empty array' do + let(:jira_users) { [] } - before do - expect(JiraImport::UsersMapper).to receive(:new).with(project, jira_users) - .and_return(double(execute: mapped_users)) + it 'retturns nil payload' do + expect(subject.success?).to be_truthy + expect(subject.payload).to be_empty + end end - it 'returns the mapped users' do - expect(subject.success?).to be_truthy - expect(subject.payload).to eq(mapped_users) + context 'when jira client returns an results' do + it 'returns the mapped users' do + expect(subject.success?).to be_truthy + expect(subject.payload).to eq(mapped_users) + end end end end end + + context 'when Jira instance is of Server deployment type' do + let(:deployment_type) { 'Server' } + let(:url) { "/rest/api/2/user/search?username=''&maxResults=50&startAt=#{start_at}" } + let(:jira_users) do + [ + { 'key' => 'acc1', 'name' => 'user1', 'emailAddress' => 'sample@jira.com' }, + { 'key' => 'acc2', 'name' => 'user2' } + ] + end + + before do + allow_next_instance_of(JiraImport::ServerUsersMapperService) do |instance| + allow(instance).to receive(:client).and_return(client) + allow(client).to receive(:get).with(url).and_return(jira_users) + end + end + + it_behaves_like 'maps jira users to gitlab users' + end + + context 'when Jira instance is of Cloud deploymet type' do + let(:deployment_type) { 'Cloud' } + let(:url) { "/rest/api/2/users?maxResults=50&startAt=#{start_at}" } + let(:jira_users) do + [ + { 'accountId' => 'acc1', 'displayName' => 'user1', 'emailAddress' => 'sample@jira.com' }, + { 'accountId' => 'acc2', 'displayName' => 'user2' } + ] + end + + before do + allow_next_instance_of(JiraImport::CloudUsersMapperService) do |instance| + allow(instance).to receive(:client).and_return(client) + allow(client).to receive(:get).with(url).and_return(jira_users) + end + end + + it_behaves_like 'maps jira users to gitlab users' + end end end diff --git a/spec/services/jira_import/users_mapper_spec.rb b/spec/services/jira_import/users_mapper_spec.rb deleted file mode 100644 index e5e8279a6fb..00000000000 --- a/spec/services/jira_import/users_mapper_spec.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe JiraImport::UsersMapper do - let_it_be(:project) { create(:project) } - - subject { described_class.new(project, jira_users).execute } - - describe '#execute' do - context 'jira_users is nil' do - let(:jira_users) { nil } - - it 'returns an empty array' do - expect(subject).to be_empty - end - end - - context 'when jira_users is present' do - let(:jira_users) do - [ - { 'accountId' => 'abcd', 'displayName' => 'user1' }, - { 'accountId' => 'efg' }, - { 'accountId' => 'hij', 'displayName' => 'user3', 'emailAddress' => 'user3@example.com' } - ] - end - - # TODO: now we only create an array in a proper format - # mapping is tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/219023 - let(:mapped_users) do - [ - { jira_account_id: 'abcd', jira_display_name: 'user1', jira_email: nil, gitlab_id: nil, gitlab_username: nil, gitlab_name: nil }, - { jira_account_id: 'efg', jira_display_name: nil, jira_email: nil, gitlab_id: nil, gitlab_username: nil, gitlab_name: nil }, - { jira_account_id: 'hij', jira_display_name: 'user3', jira_email: 'user3@example.com', gitlab_id: nil, gitlab_username: nil, gitlab_name: nil } - ] - end - - it 'returns users mapped to Gitlab' do - expect(subject).to eq(mapped_users) - end - end - end -end diff --git a/spec/support/shared_examples/services/jira_import/user_mapper_services_shared_examples.rb b/spec/support/shared_examples/services/jira_import/user_mapper_services_shared_examples.rb new file mode 100644 index 00000000000..7fc7ff8a8de --- /dev/null +++ b/spec/support/shared_examples/services/jira_import/user_mapper_services_shared_examples.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'mapping jira users' do + let(:client) { double } + + let_it_be(:project) { create(:project) } + let_it_be(:jira_service) { create(:jira_service, project: project, active: true) } + + before do + allow(subject).to receive(:client).and_return(client) + allow(client).to receive(:get).with(url).and_return(jira_users) + end + + subject { described_class.new(jira_service, start_at) } + + context 'jira_users is nil' do + let(:jira_users) { nil } + + it 'returns an empty array' do + expect(subject.execute).to be_empty + end + end + + context 'when jira_users is present' do + # TODO: now we only create an array in a proper format + # mapping is tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/219023 + let(:mapped_users) do + [ + { jira_account_id: 'abcd', jira_display_name: 'user1', jira_email: nil, gitlab_id: nil, gitlab_username: nil, gitlab_name: nil }, + { jira_account_id: 'efg', jira_display_name: nil, jira_email: nil, gitlab_id: nil, gitlab_username: nil, gitlab_name: nil }, + { jira_account_id: 'hij', jira_display_name: 'user3', jira_email: 'user3@example.com', gitlab_id: nil, gitlab_username: nil, gitlab_name: nil } + ] + end + + it 'returns users mapped to Gitlab' do + expect(subject.execute).to eq(mapped_users) + end + end +end diff --git a/yarn.lock b/yarn.lock index c05f247332a..1e928bc3c57 100644 --- a/yarn.lock +++ b/yarn.lock @@ -848,22 +848,22 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.158.0.tgz#300d416184a2b0e05f15a96547f726e1825b08a1" integrity sha512-5OJl+7TsXN9PJhY6/uwi+mTwmDZa9n/6119rf77orQ/joFYUypaYhBmy/1TcKVPsy5Zs6KCxE1kmGsfoXc1TYA== -"@gitlab/ui@18.7.0": - version "18.7.0" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-18.7.0.tgz#aee0054d50e50aaf9e7c4ea4b9e36ca4b97102bf" - integrity sha512-y1Gix1aCHvVO+zh6TCDmsCr97nLLHFnfEZRtg69EBnLBCLgwBcucC3mNeR4Q2EHTWjy/5U035UkyW6LDRX05mA== +"@gitlab/ui@20.1.1": + version "20.1.1" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-20.1.1.tgz#990ce3a0883af5c62b0f56be1e0b244b918a9159" + integrity sha512-xtWdvzC33p8i76afHtnQKuUN7fGWV89uIKfIf9/WyygXZqUFKbSW076m/9iLRxHaCYNW7ucJe3fbEW+iAgWcuA== dependencies: "@babel/standalone" "^7.0.0" "@gitlab/vue-toasted" "^1.3.0" bootstrap-vue "2.13.1" copy-to-clipboard "^3.0.8" + dompurify "^2.0.12" echarts "^4.2.1" highlight.js "^9.13.1" js-beautify "^1.8.8" lodash "^4.17.14" portal-vue "^2.1.6" resize-observer-polyfill "^1.5.1" - sanitize-html "^1.22.0" url-search-params-polyfill "^5.0.0" vue-runtime-helpers "^1.1.2" @@ -4094,7 +4094,7 @@ dom-serialize@^2.2.0: extend "^3.0.0" void-elements "^2.0.0" -dom-serializer@0, dom-serializer@^0.2.1: +dom-serializer@0: version "0.2.2" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== @@ -4143,17 +4143,10 @@ domhandler@^2.3.0: dependencies: domelementtype "1" -domhandler@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-3.0.0.tgz#51cd13efca31da95bbb0c5bee3a48300e333b3e9" - integrity sha512-eKLdI5v9m67kbXQbJSNn1zjh0SDzvzWVWtX+qEI3eMjZw8daH9k8rlj1FZY9memPwjiskQFbe7vHVVJIAqoEhw== - dependencies: - domelementtype "^2.0.1" - -dompurify@^2.0.11: - version "2.0.11" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.0.11.tgz#cd47935774230c5e478b183a572e726300b3891d" - integrity sha512-qVoGPjIW9IqxRij7klDQQ2j6nSe4UNWANBhZNLnsS7ScTtLb+3YdxkRY8brNTpkUiTtcXsCJO+jS0UCDfenLuA== +dompurify@^2.0.11, dompurify@^2.0.12: + version "2.0.12" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.0.12.tgz#284a2b041e1c60b8e72d7b4d2fadad36141254ae" + integrity sha512-Fl8KseK1imyhErHypFPA8qpq9gPzlsJ/EukA6yk9o0gX23p1TzC+rh9LqNg1qvErRTc0UNMYlKxEGSfSh43NDg== domutils@^1.5.1: version "1.6.2" @@ -4163,15 +4156,6 @@ domutils@^1.5.1: dom-serializer "0" domelementtype "1" -domutils@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.0.0.tgz#15b8278e37bfa8468d157478c58c367718133c08" - integrity sha512-n5SelJ1axbO636c2yUtOGia/IcJtVtlhQbFiVDBZHKV5ReJO1ViX7sFEemtuyoAnBxk5meNSYgA8V4s0271efg== - dependencies: - dom-serializer "^0.2.1" - domelementtype "^2.0.1" - domhandler "^3.0.0" - dot-prop@^4.1.1: version "4.2.0" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57" @@ -5843,16 +5827,6 @@ htmlparser2@^3.10.0: inherits "^2.0.1" readable-stream "^3.0.6" -htmlparser2@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-4.1.0.tgz#9a4ef161f2e4625ebf7dfbe6c0a2f52d18a59e78" - integrity sha512-4zDq1a1zhE4gQso/c5LP1OtrhYTncXNSpvJYtWJBtXAETPlMfi3IFNjGuQbYLuVY4ZR0QMqRVvo4Pdy9KLyP8Q== - dependencies: - domelementtype "^2.0.1" - domhandler "^3.0.0" - domutils "^2.0.0" - entities "^2.0.0" - http-cache-semantics@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" @@ -7646,11 +7620,6 @@ lodash.differencewith@~4.5.0: resolved "https://registry.yarnpkg.com/lodash.differencewith/-/lodash.differencewith-4.5.0.tgz#bafafbc918b55154e179176a00bb0aefaac854b7" integrity sha1-uvr7yRi1UVTheRdqALsK76rIVLc= -lodash.escaperegexp@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347" - integrity sha1-ZHYsSGGAglGKw99Mz11YhtriA0c= - lodash.find@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.find/-/lodash.find-4.6.0.tgz#cb0704d47ab71789ffa0de8b97dd926fb88b13b1" @@ -7706,11 +7675,6 @@ lodash.isplainobject@^4.0.6: resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= -lodash.isstring@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" - integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= - lodash.kebabcase@4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36" @@ -7731,11 +7695,6 @@ lodash.mapvalues@^4.6.0: resolved "https://registry.yarnpkg.com/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz#1bafa5005de9dd6f4f26668c30ca37230cc9689c" integrity sha1-G6+lAF3p3W9PJmaMMMo3IwzJaJw= -lodash.mergewith@^4.6.1: - version "4.6.2" - resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" - integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== - lodash.pick@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" @@ -9500,7 +9459,7 @@ postcss-value-parser@^4.0.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.0.tgz#99a983d365f7b2ad8d0f9b8c3094926eab4b936d" integrity sha512-ESPktioptiSUchCKgggAkzdmkgzKfmp0EU8jXH+5kbIUB+unr0Y4CY9SRMvibuvYUBjNh1ACLbxqYNpdTQOteQ== -postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.27, postcss@^7.0.5, postcss@^7.0.6, postcss@^7.0.7: +postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.5, postcss@^7.0.6, postcss@^7.0.7: version "7.0.30" resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.30.tgz#cc9378beffe46a02cbc4506a0477d05fcea9a8e2" integrity sha512-nu/0m+NtIzoubO+xdAlwZl/u5S5vi/y6BCsoL8D+8IxsD3XvBS8X4YEADNIVXKVuQvduiucnRv+vPIqj56EGMQ== @@ -10453,22 +10412,6 @@ sane@^4.0.3: minimist "^1.1.1" walker "~1.0.5" -sanitize-html@^1.22.0: - version "1.22.0" - resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.22.0.tgz#9df779c53cf5755adb2322943c21c1c1dffca7bf" - integrity sha512-3RPo65mbTKpOAdAYWU496MSty1YbB3Y5bjwL5OclgaSSMtv65xvM7RW/EHRumzaZ1UddEJowCbSdK0xl5sAu0A== - dependencies: - chalk "^2.4.1" - htmlparser2 "^4.1.0" - lodash.clonedeep "^4.5.0" - lodash.escaperegexp "^4.1.2" - lodash.isplainobject "^4.0.6" - lodash.isstring "^4.0.1" - lodash.mergewith "^4.6.1" - postcss "^7.0.27" - srcset "^2.0.1" - xtend "^4.0.1" - sass-graph@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49" @@ -10968,11 +10911,6 @@ sql.js@^0.4.0: resolved "https://registry.yarnpkg.com/sql.js/-/sql.js-0.4.0.tgz#23be9635520eb0ff43a741e7e830397266e88445" integrity sha1-I76WNVIOsP9Dp0Hn6DA5cmbohEU= -srcset@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/srcset/-/srcset-2.0.1.tgz#8f842d357487eb797f413d9c309de7a5149df5ac" - integrity sha512-00kZI87TdRKwt+P8jj8UZxbfp7mK2ufxcIMWvhAOZNJTRROimpHeruWrGvCZneiuVDLqdyHefVp748ECTnyUBQ== - sshpk@^1.7.0: version "1.15.2" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.15.2.tgz#c946d6bd9b1a39d0e8635763f5242d6ed6dcb629" |