diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-26 21:08:59 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-26 21:08:59 +0300 |
commit | 5f825c2edec69e9a23e100c949c6c40e88ae5235 (patch) | |
tree | dc510f698f6885ae3656da30eb4c4e5ab4f5dd58 | |
parent | c46e0d0c271a21b67a3412faf750d27dd63432bb (diff) |
Add latest changes from gitlab-org/gitlab@master
101 files changed, 1470 insertions, 1154 deletions
diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS index 5db7ba5b109..9a8c93f5103 100644 --- a/.gitlab/CODEOWNERS +++ b/.gitlab/CODEOWNERS @@ -1236,7 +1236,6 @@ lib/gitlab/checks/** /app/controllers/projects/triggers_controller.rb /app/controllers/projects/variables_controller.rb /app/models/commit_status.rb -/app/models/external_pull_request.rb /app/models/generic_commit_status.rb /app/models/namespace_ci_cd_setting.rb /app/models/project_ci_cd_setting.rb diff --git a/.gitlab/ci/qa.gitlab-ci.yml b/.gitlab/ci/qa.gitlab-ci.yml index 6d181a59214..9089facb3a8 100644 --- a/.gitlab/ci/qa.gitlab-ci.yml +++ b/.gitlab/ci/qa.gitlab-ci.yml @@ -27,7 +27,6 @@ RUN_WITH_BUNDLE: "true" # instructs pipeline to install and run gitlab-qa gem via bundler QA_PATH: qa # sets the optional path for bundler to run from DYNAMIC_PIPELINE_YML: package-and-test-pipeline.yml # yml files are generated by scripts/generate-e2e-pipeline script - EXTRA_GITLAB_QA_OPTS: --set-feature-flags super_sidebar_nav_enrolled=enabled inherit: variables: - CHROME_VERSION diff --git a/.rubocop_todo/layout/space_in_lambda_literal.yml b/.rubocop_todo/layout/space_in_lambda_literal.yml index 3bc63853043..ca6803be7e8 100644 --- a/.rubocop_todo/layout/space_in_lambda_literal.yml +++ b/.rubocop_todo/layout/space_in_lambda_literal.yml @@ -46,7 +46,6 @@ Layout/SpaceInLambdaLiteral: - 'app/models/environment.rb' - 'app/models/error_tracking/client_key.rb' - 'app/models/error_tracking/error.rb' - - 'app/models/external_pull_request.rb' - 'app/models/group.rb' - 'app/models/group_group_link.rb' - 'app/models/incident_management/timeline_event_tag.rb' diff --git a/.rubocop_todo/performance/active_record_subtransaction_methods.yml b/.rubocop_todo/performance/active_record_subtransaction_methods.yml index 0c0a527a065..a61bac45eff 100644 --- a/.rubocop_todo/performance/active_record_subtransaction_methods.yml +++ b/.rubocop_todo/performance/active_record_subtransaction_methods.yml @@ -9,7 +9,7 @@ Performance/ActiveRecordSubtransactionMethods: - 'app/models/container_repository.rb' - 'app/models/design_management/design_collection.rb' - 'app/models/error_tracking/error.rb' - - 'app/models/external_pull_request.rb' + - 'app/models/ci/external_pull_request.rb' - 'app/models/plan.rb' - 'app/models/project.rb' - 'app/models/shard.rb' diff --git a/.rubocop_todo/rspec/context_wording.yml b/.rubocop_todo/rspec/context_wording.yml index 1ce003ecd2f..64b2c69e6de 100644 --- a/.rubocop_todo/rspec/context_wording.yml +++ b/.rubocop_todo/rspec/context_wording.yml @@ -1535,7 +1535,6 @@ RSpec/ContextWording: - 'spec/lib/api/validations/validators/untrusted_regexp_spec.rb' - 'spec/lib/atlassian/jira_connect/jwt/asymmetric_spec.rb' - 'spec/lib/atlassian/jira_connect/jwt/symmetric_spec.rb' - - 'spec/lib/atlassian/jira_connect/serializers/deployment_entity_spec.rb' - 'spec/lib/atlassian/jira_connect/serializers/feature_flag_entity_spec.rb' - 'spec/lib/atlassian/jira_connect_spec.rb' - 'spec/lib/backup/gitaly_backup_spec.rb' @@ -2200,7 +2199,6 @@ RSpec/ContextWording: - 'spec/models/environment_status_spec.rb' - 'spec/models/error_tracking/error_spec.rb' - 'spec/models/event_spec.rb' - - 'spec/models/external_pull_request_spec.rb' - 'spec/models/gpg_key_spec.rb' - 'spec/models/grafana_integration_spec.rb' - 'spec/models/group_label_spec.rb' diff --git a/.rubocop_todo/rspec/missing_feature_category.yml b/.rubocop_todo/rspec/missing_feature_category.yml index 3cf0ed5c38c..7413252f590 100644 --- a/.rubocop_todo/rspec/missing_feature_category.yml +++ b/.rubocop_todo/rspec/missing_feature_category.yml @@ -4787,7 +4787,6 @@ RSpec/MissingFeatureCategory: - 'spec/models/event_collection_spec.rb' - 'spec/models/exported_protected_branch_spec.rb' - 'spec/models/external_issue_spec.rb' - - 'spec/models/external_pull_request_spec.rb' - 'spec/models/fork_network_member_spec.rb' - 'spec/models/fork_network_spec.rb' - 'spec/models/generic_commit_status_spec.rb' diff --git a/.rubocop_todo/style/guard_clause.yml b/.rubocop_todo/style/guard_clause.yml index 8613e240d9c..abd7fe7af98 100644 --- a/.rubocop_todo/style/guard_clause.yml +++ b/.rubocop_todo/style/guard_clause.yml @@ -105,7 +105,6 @@ Style/GuardClause: - 'app/models/diff_viewer/base.rb' - 'app/models/environment.rb' - 'app/models/error_tracking/project_error_tracking_setting.rb' - - 'app/models/external_pull_request.rb' - 'app/models/generic_commit_status.rb' - 'app/models/grafana_integration.rb' - 'app/models/integrations/base_issue_tracker.rb' diff --git a/.rubocop_todo/style/if_unless_modifier.yml b/.rubocop_todo/style/if_unless_modifier.yml index 2ca9b22f0ad..09707e68648 100644 --- a/.rubocop_todo/style/if_unless_modifier.yml +++ b/.rubocop_todo/style/if_unless_modifier.yml @@ -155,7 +155,6 @@ Style/IfUnlessModifier: - 'app/models/diff_viewer/base.rb' - 'app/models/environment.rb' - 'app/models/error_tracking/project_error_tracking_setting.rb' - - 'app/models/external_pull_request.rb' - 'app/models/generic_commit_status.rb' - 'app/models/grafana_integration.rb' - 'app/models/group.rb' diff --git a/.rubocop_todo/style/sole_nested_conditional.yml b/.rubocop_todo/style/sole_nested_conditional.yml index 65cac595cde..975da41a69a 100644 --- a/.rubocop_todo/style/sole_nested_conditional.yml +++ b/.rubocop_todo/style/sole_nested_conditional.yml @@ -8,7 +8,6 @@ Style/SoleNestedConditional: - 'app/controllers/projects/blob_controller.rb' - 'app/helpers/nav_helper.rb' - 'app/models/concerns/cache_markdown_field.rb' - - 'app/models/external_pull_request.rb' - 'app/models/issue.rb' - 'app/models/network/graph.rb' - 'app/models/packages/package.rb' diff --git a/app/assets/javascripts/batch_comments/components/submit_dropdown.vue b/app/assets/javascripts/batch_comments/components/submit_dropdown.vue index e6c3a0cba58..db676b36a0b 100644 --- a/app/assets/javascripts/batch_comments/components/submit_dropdown.vue +++ b/app/assets/javascripts/batch_comments/components/submit_dropdown.vue @@ -124,7 +124,7 @@ export default { :textarea-value="noteData.note" :markdown-preview-path="getNoteableData.preview_note_path" :markdown-docs-path="getNotesData.markdownDocsPath" - :quick-actions-docs-path="getNotesData.quickActionsDocsPath" + supports-quick-actions :restricted-tool-bar-items="$options.restrictedToolbarItems" :force-autosize="false" class="js-no-autosize" diff --git a/app/assets/javascripts/behaviors/preview_markdown.js b/app/assets/javascripts/behaviors/preview_markdown.js index bcd92d09033..ce77ede9fe4 100644 --- a/app/assets/javascripts/behaviors/preview_markdown.js +++ b/app/assets/javascripts/behaviors/preview_markdown.js @@ -139,7 +139,7 @@ $(document).on('markdown-preview:show', (e, $form) => { // toggle content $form.find('.md-write-holder').hide(); $form.find('.md-preview-holder').show(); - $form.find('.md-header-toolbar, .js-zen-enter').addClass('gl-display-none!'); + $form.find('.haml-markdown-button, .js-zen-enter').addClass('gl-display-none!'); markdownPreview.showPreview($form); }); @@ -162,7 +162,7 @@ $(document).on('markdown-preview:hide', (e, $form) => { $form.find('.md-write-holder').show(); $form.find('textarea.markdown-area').focus(); $form.find('.md-preview-holder').hide(); - $form.find('.md-header-toolbar, .js-zen-enter').removeClass('gl-display-none!'); + $form.find('.haml-markdown-button, .js-zen-enter').removeClass('gl-display-none!'); markdownPreview.hideReferencedCommands($form); }); diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue index 92f3c3fb8fa..b9e713cee6e 100644 --- a/app/assets/javascripts/content_editor/components/content_editor.vue +++ b/app/assets/javascripts/content_editor/components/content_editor.vue @@ -1,8 +1,9 @@ <script> +import { GlButton } from '@gitlab/ui'; import { EditorContent as TiptapEditorContent } from '@tiptap/vue-2'; -import { GlSprintf, GlLink } from '@gitlab/ui'; -import { __, s__ } from '~/locale'; +import { __ } from '~/locale'; import { VARIANT_DANGER } from '~/alert'; +import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue'; import { createContentEditor } from '../services/create_content_editor'; import { ALERT_EVENT, TIPTAP_AUTOFOCUS_OPTIONS } from '../constants'; import ContentEditorAlert from './content_editor_alert.vue'; @@ -17,8 +18,7 @@ import LoadingIndicator from './loading_indicator.vue'; export default { components: { - GlSprintf, - GlLink, + GlButton, LoadingIndicator, ContentEditorAlert, ContentEditorProvider, @@ -29,12 +29,17 @@ export default { MediaBubbleMenu, EditorStateObserver, ReferenceBubbleMenu, + EditorModeSwitcher, }, props: { renderMarkdown: { type: Function, required: true, }, + markdownDocsPath: { + type: String, + required: true, + }, uploadsPath: { type: String, required: true, @@ -65,10 +70,10 @@ export default { default: false, validator: (autofocus) => TIPTAP_AUTOFOCUS_OPTIONS.includes(autofocus), }, - quickActionsDocsPath: { - type: String, + supportsQuickActions: { + type: Boolean, required: false, - default: '', + default: false, }, drawioEnabled: { type: Boolean, @@ -204,11 +209,9 @@ export default { markdown: this.latestMarkdown, }); }, - }, - i18n: { - quickActionsText: s__( - 'ContentEditor|For %{quickActionsDocsLinkStart}quick actions%{quickActionsDocsLinkEnd}, type %{keyboardStart}/%{keyboardEnd}.', - ), + handleEditorModeChanged() { + this.$emit('enableMarkdownEditor'); + }, }, }; </script> @@ -230,6 +233,7 @@ export default { > <formatting-toolbar ref="toolbar" + :supports-quick-actions="supportsQuickActions" :hide-attachment-button="disableAttachments" @enableMarkdownEditor="$emit('enableMarkdownEditor')" /> @@ -249,21 +253,16 @@ export default { <reference-bubble-menu /> </div> <div - v-if="quickActionsDocsPath" - class="gl-display-flex gl-align-items-center gl-rounded-bottom-left-base gl-rounded-bottom-right-base gl-px-4 gl-mx-2 gl-mb-2 gl-bg-gray-10 gl-text-secondary" + class="gl-display-flex gl-display-flex gl-flex-direction-row gl-justify-content-space-between gl-align-items-center gl-rounded-bottom-left-base gl-rounded-bottom-right-base gl-px-2 gl-mx-2 gl-mb-2 gl-bg-gray-10 gl-text-secondary" > - <div class="gl-w-full gl-line-height-32 gl-font-sm"> - <gl-sprintf :message="$options.i18n.quickActionsText"> - <template #keyboard="{ content }"> - <kbd>{{ content }}</kbd> - </template> - <template #quickActionsDocsLink="{ content }"> - <gl-link :href="quickActionsDocsPath" target="_blank" class="gl-font-sm">{{ - content - }}</gl-link> - </template> - </gl-sprintf> - </div> + <editor-mode-switcher size="small" value="richText" @input="handleEditorModeChanged" /> + <gl-button + icon="markdown-mark" + :href="markdownDocsPath" + target="_blank" + category="tertiary" + size="small" + /> </div> </div> </content-editor-provider> diff --git a/app/assets/javascripts/content_editor/components/formatting_toolbar.vue b/app/assets/javascripts/content_editor/components/formatting_toolbar.vue index c53007b68cf..8bf71a481bd 100644 --- a/app/assets/javascripts/content_editor/components/formatting_toolbar.vue +++ b/app/assets/javascripts/content_editor/components/formatting_toolbar.vue @@ -1,5 +1,4 @@ <script> -import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue'; import trackUIControl from '../services/track_ui_control'; import ToolbarButton from './toolbar_button.vue'; import ToolbarAttachmentButton from './toolbar_attachment_button.vue'; @@ -14,9 +13,13 @@ export default { ToolbarTableButton, ToolbarAttachmentButton, ToolbarMoreDropdown, - EditorModeSwitcher, }, props: { + supportsQuickActions: { + type: Boolean, + required: false, + default: false, + }, hideAttachmentButton: { type: Boolean, default: false, @@ -27,9 +30,6 @@ export default { trackToolbarControlExecution({ contentType, value }) { trackUIControl({ property: contentType, value }); }, - handleEditorModeChanged() { - this.$emit('enableMarkdownEditor'); - }, }, }; </script> @@ -125,11 +125,19 @@ export default { data-testid="attachment" @execute="trackToolbarControlExecution" /> + <!-- TODO Add icon and trigger functionality from here --> + <toolbar-button + v-if="supportsQuickActions" + data-testid="quick-actions" + content-type="quickAction" + icon-name="quick-actions" + class="gl-display-none gl-sm-display-inline gl-mr-1!" + editor-command="insertQuickAction" + :label="__('Add a quick action')" + @execute="trackToolbarControlExecution" + /> <toolbar-more-dropdown data-testid="more" @execute="trackToolbarControlExecution" /> </div> - <div class="content-editor-switcher gl-display-flex gl-align-items-center gl-ml-auto"> - <editor-mode-switcher size="small" value="richText" @input="handleEditorModeChanged" /> - </div> </div> </div> </template> diff --git a/app/assets/javascripts/content_editor/components/toolbar_attachment_button.vue b/app/assets/javascripts/content_editor/components/toolbar_attachment_button.vue index 1e13c17bc38..4cf150dd948 100644 --- a/app/assets/javascripts/content_editor/components/toolbar_attachment_button.vue +++ b/app/assets/javascripts/content_editor/components/toolbar_attachment_button.vue @@ -47,6 +47,7 @@ export default { category="tertiary" icon="paperclip" size="small" + class="gl-mr-3" lazy @click="openFileUpload" /> diff --git a/app/assets/javascripts/content_editor/components/toolbar_table_button.vue b/app/assets/javascripts/content_editor/components/toolbar_table_button.vue index eb7985f628a..771455dfe66 100644 --- a/app/assets/javascripts/content_editor/components/toolbar_table_button.vue +++ b/app/assets/javascripts/content_editor/components/toolbar_table_button.vue @@ -88,10 +88,11 @@ export default { size="small" category="tertiary" icon="table" + no-caret :aria-label="__('Insert table')" :toggle-text="__('Insert table')" positioning-strategy="fixed" - class="content-editor-table-dropdown" + class="content-editor-table-dropdown gl-mr-3" text-sr-only :fluid-width="true" @shown="setFocus(1, 1)" diff --git a/app/assets/javascripts/content_editor/extensions/reference.js b/app/assets/javascripts/content_editor/extensions/reference.js index ef69b9bbda6..fd248709b5a 100644 --- a/app/assets/javascripts/content_editor/extensions/reference.js +++ b/app/assets/javascripts/content_editor/extensions/reference.js @@ -63,6 +63,12 @@ export default Node.create({ }; }, + addCommands() { + return { + insertQuickAction: () => ({ commands }) => commands.insertContent('<p>/</p>'), + }; + }, + addInputRules() { const { editor } = this; const { assetResolver } = this.options; diff --git a/app/assets/javascripts/invite_members/components/members_token_select.vue b/app/assets/javascripts/invite_members/components/members_token_select.vue index e0bfa1111e8..8493787f075 100644 --- a/app/assets/javascripts/invite_members/components/members_token_select.vue +++ b/app/assets/javascripts/invite_members/components/members_token_select.vue @@ -59,6 +59,7 @@ export default { return { loading: false, query: '', + originalInput: '', users: [], selectedTokens: [], hasBeenFocused: false, @@ -67,9 +68,9 @@ export default { }, computed: { emailIsValid() { - const regex = /.+@/; + const regex = /^\S+@\S+$/; - return this.query.match(regex) !== null; + return this.originalInput.match(regex) !== null; }, placeholderText() { if (this.selectedTokens.length === 0) { @@ -116,6 +117,7 @@ export default { methods: { handleTextInput(inputQuery) { this.hideDropdownWithNoItems = false; + this.originalInput = inputQuery; this.query = inputQuery.trim(); this.loading = true; this.retrieveUsers(); diff --git a/app/assets/javascripts/issues/show/components/fields/description.vue b/app/assets/javascripts/issues/show/components/fields/description.vue index c8ea8fb7ab2..3969c3b84a7 100644 --- a/app/assets/javascripts/issues/show/components/fields/description.vue +++ b/app/assets/javascripts/issues/show/components/fields/description.vue @@ -1,7 +1,6 @@ <script> import { __ } from '~/locale'; import MarkdownField from '~/vue_shared/components/markdown/field.vue'; -import { helpPagePath } from '~/helpers/help_page_helper'; import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue'; import glFeaturesFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import updateMixin from '../../mixins/update'; @@ -46,11 +45,6 @@ export default { }, }; }, - computed: { - quickActionsDocsPath() { - return helpPagePath('user/project/quick_actions'); - }, - }, mounted() { this.focus(); }, @@ -72,7 +66,6 @@ export default { :render-markdown-path="markdownPreviewPath" :markdown-docs-path="markdownDocsPath" :form-field-props="formFieldProps" - :quick-actions-docs-path="quickActionsDocsPath" :enable-autocomplete="enableAutocomplete" supports-quick-actions autofocus @@ -85,7 +78,7 @@ export default { class="gl-mt-3" :markdown-preview-path="markdownPreviewPath" :markdown-docs-path="markdownDocsPath" - :quick-actions-docs-path="quickActionsDocsPath" + supports-quick-actions :can-attach-file="canAttachFile" :enable-autocomplete="enableAutocomplete" :textarea-value="value" diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index a2873622682..5fa903568a1 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -376,8 +376,8 @@ export function updateText({ textArea, tag, cursorOffset, blockTag, wrap, select textArea = $textArea.get(0); const text = $textArea.val(); const selected = selectedText(text, textArea) || tagContent; - $textArea.focus(); - return insertMarkdownText({ + textArea.focus(); + insertMarkdownText({ textArea, text, tag, @@ -387,6 +387,7 @@ export function updateText({ textArea, tag, cursorOffset, blockTag, wrap, select wrap, select, }); + textArea.click(); } /** diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index cba0f960c00..27ec8b55da7 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -146,9 +146,6 @@ export default { markdownDocsPath() { return this.getNotesData.markdownDocsPath; }, - quickActionsDocsPath() { - return this.getNotesData.quickActionsDocsPath; - }, markdownPreviewPath() { return this.getNoteableData.preview_note_path; }, @@ -366,7 +363,6 @@ export default { :render-markdown-path="markdownPreviewPath" :markdown-docs-path="markdownDocsPath" :add-spacing-classes="false" - :quick-actions-docs-path="quickActionsDocsPath" :form-field-props="formFieldProps" :autosave-key="autosaveKey" :disabled="isSubmitting" diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index fe7967f1ed0..45b70d78225 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -192,9 +192,6 @@ export default { markdownDocsPath() { return this.getNotesDataByProp('markdownDocsPath'); }, - quickActionsDocsPath() { - return this.getNotesDataByProp('quickActionsDocsPath'); - }, currentUserId() { return this.getUserDataByProp('id'); }, @@ -359,7 +356,6 @@ export default { :note="discussionNote" :form-field-props="formFieldProps" :show-suggest-popover="showSuggestPopover" - :quick-actions-docs-path="quickActionsDocsPath" :autosave-key="autosaveKey" :autocomplete-data-sources="autocompleteDataSources" :disabled="isSubmitting" diff --git a/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue b/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue index 186f5619b87..cc9790279cd 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue @@ -81,13 +81,14 @@ export default { :items="filteredSavedReplies" :toggle-text="__('Insert comment template')" text-sr-only + no-caret toggle-class="js-comment-template-toggle" icon="comment-lines" category="tertiary" placement="right" searchable size="small" - class="comment-template-dropdown" + class="comment-template-dropdown gl-mr-3" positioning-strategy="fixed" :searching="$apollo.queries.savedReplies.loading" @shown="fetchCommentTemplates" diff --git a/app/assets/javascripts/vue_shared/components/markdown/editor_mode_switcher.vue b/app/assets/javascripts/vue_shared/components/markdown/editor_mode_switcher.vue index 645975ca565..5cf5fbd5323 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/editor_mode_switcher.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/editor_mode_switcher.vue @@ -17,16 +17,20 @@ export default { return this.value === 'markdown'; }, text() { - return this.markdownEditorSelected ? __('Switch to rich text') : __('Switch to Markdown'); + return this.markdownEditorSelected + ? __('Switch to rich text editing') + : __('Switch to plain text editing'); }, }, }; </script> <template> - <gl-button - class="btn btn-default btn-sm gl-button btn-default-tertiary" - data-qa-selector="editing_mode_switcher" - @click="$emit('input')" - >{{ text }}</gl-button - > + <div class="content-editor-switcher gl-display-inline-flex gl-align-items-center"> + <gl-button + class="btn btn-default btn-sm gl-button btn-default-tertiary" + data-qa-selector="editing_mode_switcher" + @click="$emit('input')" + >{{ text }}</gl-button + > + </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index b5a31f42ec7..268352a9c9c 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -68,10 +68,10 @@ export default { required: false, default: false, }, - quickActionsDocsPath: { - type: String, + supportsQuickActions: { + type: Boolean, required: false, - default: '', + default: false, }, canAttachFile: { type: Boolean, @@ -371,13 +371,12 @@ export default { :uploads-path="uploadsPath" :markdown-preview-path="markdownPreviewPath" :drawio-enabled="drawioEnabled" + :supports-quick-actions="supportsQuickActions" data-testid="markdownHeader" :restricted-tool-bar-items="restrictedToolBarItems" - :show-content-editor-switcher="showContentEditorSwitcher" @showPreview="showPreview" @hidePreview="hidePreview" @handleSuggestDismissed="() => $emit('handleSuggestDismissed')" - @enableContentEditor="$emit('enableContentEditor')" /> <div v-show="!previewMarkdown" class="md-write-holder"> <div class="zen-backdrop"> @@ -391,9 +390,10 @@ export default { </a> <markdown-toolbar :markdown-docs-path="markdownDocsPath" - :quick-actions-docs-path="quickActionsDocsPath" :can-attach-file="canAttachFile" :show-comment-tool-bar="showCommentToolBar" + :show-content-editor-switcher="showContentEditorSwitcher" + @enableContentEditor="$emit('enableContentEditor')" /> </div> </div> diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index af0b34f1389..bf070943fe6 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -19,7 +19,6 @@ import { updateText } from '~/lib/utils/text_markdown'; import ToolbarButton from './toolbar_button.vue'; import DrawioToolbarButton from './drawio_toolbar_button.vue'; import CommentTemplatesDropdown from './comment_templates_dropdown.vue'; -import EditorModeSwitcher from './editor_mode_switcher.vue'; export default { components: { @@ -29,7 +28,6 @@ export default { DrawioToolbarButton, CommentTemplatesDropdown, AiActionsDropdown: () => import('ee_component/ai/components/ai_actions_dropdown.vue'), - EditorModeSwitcher, }, directives: { GlTooltip: GlTooltipDirective, @@ -91,7 +89,7 @@ export default { required: false, default: false, }, - showContentEditorSwitcher: { + supportsQuickActions: { type: Boolean, required: false, default: false, @@ -126,9 +124,6 @@ export default { const expandText = s__('MarkdownEditor|Click to expand'); return [`<details><summary>${expandText}</summary>`, `{text}`, '</details>'].join('\n'); }, - showEditorModeSwitcher() { - return this.showContentEditorSwitcher && !this.previewMarkdown; - }, }, watch: { showSuggestPopover() { @@ -208,9 +203,6 @@ export default { }); } }, - handleEditorModeChanged() { - this.$emit('enableContentEditor'); - }, switchPreview() { if (this.previewMarkdown) { this.hideMarkdownPreview(); @@ -236,233 +228,228 @@ export default { <template> <div class="md-header gl-bg-gray-50 gl-px-2 gl-rounded-base gl-mx-2 gl-mt-2"> - <div - class="gl-display-flex gl-align-items-center gl-flex-wrap" - :class="{ - 'gl-justify-content-end': previewMarkdown, - 'gl-justify-content-space-between': !previewMarkdown, - }" - > + <div class="gl-display-flex gl-align-items-center gl-flex-wrap"> <div data-testid="md-header-toolbar" - class="md-header-toolbar gl-display-flex gl-py-2 gl-flex-wrap" - :class="{ 'gl-display-none!': previewMarkdown }" + class="md-header-toolbar gl-display-flex gl-py-2 gl-flex-wrap gl-row-gap-3" > - <template v-if="canSuggest"> - <toolbar-button - ref="suggestButton" - :tag="mdSuggestion" - :prepend="true" - :button-title="__('Insert suggestion')" - :cursor-offset="4" - :tag-content="lineContent" - icon="doc-code" - data-qa-selector="suggestion_button" - class="js-suggestion-btn" - @click="handleSuggestDismissed" - /> - <gl-popover - v-if="suggestPopoverVisible" - :target="$refs.suggestButton.$el" - :css-classes="['diff-suggest-popover']" - placement="bottom" - :show="suggestPopoverVisible" - triggers="" - > - <strong>{{ __('New! Suggest changes directly') }}</strong> - <p class="mb-2"> - {{ - __( - 'Suggest code changes which can be immediately applied in one click. Try it out!', - ) - }} - </p> - <gl-button - variant="confirm" - category="primary" - size="small" - data-qa-selector="dismiss_suggestion_popover_button" - @click="handleSuggestDismissed" - > - {{ __('Got it') }} - </gl-button> - </gl-popover> - </template> - <ai-actions-dropdown - v-if="editorAiActions.length" - :actions="editorAiActions" - @input="insertIntoTextarea" - /> - <toolbar-button - tag="**" - :button-title=" - /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ - sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), { - modifierKey, - }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */ - " - :shortcuts="$options.shortcuts.bold" - icon="bold" - /> - <toolbar-button - tag="_" - :button-title=" - /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ - sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), { - modifierKey, - }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */ - " - :shortcuts="$options.shortcuts.italic" - icon="italic" - /> - <toolbar-button - v-if="!restrictedToolBarItems.includes('strikethrough')" - tag="~~" - :button-title=" - /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ - sprintf(s__('MarkdownEditor|Add strikethrough text (%{modifierKey}⇧X)'), { - modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */, - }) - " - :shortcuts="$options.shortcuts.strikethrough" - icon="strikethrough" - /> - <toolbar-button - v-if="!restrictedToolBarItems.includes('quote')" - :prepend="true" - :tag="tag" - :button-title="__('Insert a quote')" - icon="quote" - @click="handleQuote" - /> - <toolbar-button tag="`" tag-block="```" :button-title="__('Insert code')" icon="code" /> - <toolbar-button - tag="[{text}](url)" - tag-select="url" - :button-title=" - /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ - sprintf(s__('MarkdownEditor|Add a link (%{modifierKey}K)'), { - modifierKey, - }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */ - " - :shortcuts="$options.shortcuts.link" - icon="link" - /> - <toolbar-button - v-if="!restrictedToolBarItems.includes('bullet-list')" - :prepend="true" - tag="- " - :button-title="__('Add a bullet list')" - icon="list-bulleted" - /> - <toolbar-button - v-if="!restrictedToolBarItems.includes('numbered-list')" - :prepend="true" - tag="1. " - :button-title="__('Add a numbered list')" - icon="list-numbered" - /> - <toolbar-button - v-if="!restrictedToolBarItems.includes('task-list')" - :prepend="true" - tag="- [ ] " - :button-title="__('Add a checklist')" - icon="list-task" - /> - <toolbar-button - v-if="!restrictedToolBarItems.includes('indent')" - class="gl-display-none" - :button-title=" - /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ - sprintf(s__('MarkdownEditor|Indent line (%{modifierKey}])'), { - modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */, - }) - " - :shortcuts="$options.shortcuts.indent" - command="indentLines" - icon="list-indent" - /> - <toolbar-button - v-if="!restrictedToolBarItems.includes('outdent')" - class="gl-display-none" - :button-title=" - /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ - sprintf(s__('MarkdownEditor|Outdent line (%{modifierKey}[)'), { - modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */, - }) - " - :shortcuts="$options.shortcuts.outdent" - command="outdentLines" - icon="list-outdent" - /> - <toolbar-button - v-if="!restrictedToolBarItems.includes('collapsible-section')" - :tag="mdCollapsibleSection" - :prepend="true" - tag-select="Click to expand" - :button-title="__('Add a collapsible section')" - icon="details-block" - /> - <toolbar-button - v-if="!restrictedToolBarItems.includes('table')" - :tag="mdTable" - :prepend="true" - :button-title="__('Add a table')" - icon="table" - /> - <gl-button - v-if="!restrictedToolBarItems.includes('attach-file')" - v-gl-tooltip - :aria-label="__('Attach a file or image')" - :title="__('Attach a file or image')" - class="gl-mr-2" - data-testid="button-attach-file" - category="tertiary" - icon="paperclip" - size="small" - @click="handleAttachFile" - /> - <drawio-toolbar-button - v-if="drawioEnabled" - :uploads-path="uploadsPath" - :markdown-preview-path="markdownPreviewPath" - /> - <comment-templates-dropdown - v-if="newCommentTemplatePath && glFeatures.savedReplies" - :new-comment-template-path="newCommentTemplatePath" - /> - </div> - <div class="switch-preview gl-py-2 gl-display-flex gl-align-items-center gl-ml-auto"> - <editor-mode-switcher - v-if="showEditorModeSwitcher" - size="small" - class="gl-mr-2" - value="markdown" - @input="handleEditorModeChanged" - /> <gl-button v-if="enablePreview" data-testid="preview-toggle" value="preview" :label="$options.i18n.previewTabTitle" - class="js-md-preview-button gl-flex-direction-row-reverse gl-align-items-center gl-font-weight-normal!" + class="js-md-preview-button gl-flex-direction-row-reverse gl-align-items-center gl-font-weight-normal! gl-mr-2" size="small" category="tertiary" @click="switchPreview" >{{ previewMarkdown ? $options.i18n.hidePreview : $options.i18n.preview }}</gl-button > - <gl-button - v-if="!restrictedToolBarItems.includes('full-screen')" - v-gl-tooltip - :class="{ 'gl-display-none!': previewMarkdown }" - class="js-zen-enter gl-ml-2" - category="tertiary" - icon="maximize" - size="small" - :title="__('Go full screen')" - :prepend="true" - :aria-label="__('Go full screen')" - /> + <template v-if="!previewMarkdown"> + <template v-if="canSuggest"> + <toolbar-button + ref="suggestButton" + :tag="mdSuggestion" + :prepend="true" + :button-title="__('Insert suggestion')" + :cursor-offset="4" + :tag-content="lineContent" + icon="doc-code" + data-qa-selector="suggestion_button" + class="js-suggestion-btn" + @click="handleSuggestDismissed" + /> + <gl-popover + v-if="suggestPopoverVisible" + :target="$refs.suggestButton.$el" + :css-classes="['diff-suggest-popover']" + placement="bottom" + :show="suggestPopoverVisible" + triggers="" + > + <strong>{{ __('New! Suggest changes directly') }}</strong> + <p class="mb-2"> + {{ + __( + 'Suggest code changes which can be immediately applied in one click. Try it out!', + ) + }} + </p> + <gl-button + variant="confirm" + category="primary" + size="small" + data-qa-selector="dismiss_suggestion_popover_button" + @click="handleSuggestDismissed" + > + {{ __('Got it') }} + </gl-button> + </gl-popover> + </template> + <ai-actions-dropdown + v-if="editorAiActions.length" + :actions="editorAiActions" + @input="insertIntoTextarea" + /> + <toolbar-button + tag="**" + :button-title=" + /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ + sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), { + modifierKey, + }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */ + " + :shortcuts="$options.shortcuts.bold" + icon="bold" + /> + <toolbar-button + tag="_" + :button-title=" + /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ + sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), { + modifierKey, + }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */ + " + :shortcuts="$options.shortcuts.italic" + icon="italic" + /> + <toolbar-button + v-if="!restrictedToolBarItems.includes('strikethrough')" + tag="~~" + :button-title=" + /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ + sprintf(s__('MarkdownEditor|Add strikethrough text (%{modifierKey}⇧X)'), { + modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */, + }) + " + :shortcuts="$options.shortcuts.strikethrough" + icon="strikethrough" + /> + <toolbar-button + v-if="!restrictedToolBarItems.includes('quote')" + :prepend="true" + :tag="tag" + :button-title="__('Insert a quote')" + icon="quote" + @click="handleQuote" + /> + <toolbar-button tag="`" tag-block="```" :button-title="__('Insert code')" icon="code" /> + <toolbar-button + tag="[{text}](url)" + tag-select="url" + :button-title=" + /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ + sprintf(s__('MarkdownEditor|Add a link (%{modifierKey}K)'), { + modifierKey, + }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */ + " + :shortcuts="$options.shortcuts.link" + icon="link" + /> + <toolbar-button + v-if="!restrictedToolBarItems.includes('bullet-list')" + :prepend="true" + tag="- " + :button-title="__('Add a bullet list')" + icon="list-bulleted" + /> + <toolbar-button + v-if="!restrictedToolBarItems.includes('numbered-list')" + :prepend="true" + tag="1. " + :button-title="__('Add a numbered list')" + icon="list-numbered" + /> + <toolbar-button + v-if="!restrictedToolBarItems.includes('task-list')" + :prepend="true" + tag="- [ ] " + :button-title="__('Add a checklist')" + icon="list-task" + /> + <toolbar-button + v-if="!restrictedToolBarItems.includes('indent')" + class="gl-display-none" + :button-title=" + /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ + sprintf(s__('MarkdownEditor|Indent line (%{modifierKey}])'), { + modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */, + }) + " + :shortcuts="$options.shortcuts.indent" + command="indentLines" + icon="list-indent" + /> + <toolbar-button + v-if="!restrictedToolBarItems.includes('outdent')" + class="gl-display-none" + :button-title=" + /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ + sprintf(s__('MarkdownEditor|Outdent line (%{modifierKey}[)'), { + modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */, + }) + " + :shortcuts="$options.shortcuts.outdent" + command="outdentLines" + icon="list-outdent" + /> + <toolbar-button + v-if="!restrictedToolBarItems.includes('collapsible-section')" + :tag="mdCollapsibleSection" + :prepend="true" + tag-select="Click to expand" + :button-title="__('Add a collapsible section')" + icon="details-block" + /> + <toolbar-button + v-if="!restrictedToolBarItems.includes('table')" + :tag="mdTable" + :prepend="true" + :button-title="__('Add a table')" + icon="table" + /> + <gl-button + v-if="!restrictedToolBarItems.includes('attach-file')" + v-gl-tooltip + :aria-label="__('Attach a file or image')" + :title="__('Attach a file or image')" + class="gl-mr-3" + data-testid="button-attach-file" + category="tertiary" + icon="paperclip" + size="small" + @click="handleAttachFile" + /> + <drawio-toolbar-button + v-if="drawioEnabled" + :uploads-path="uploadsPath" + :markdown-preview-path="markdownPreviewPath" + /> + <!-- TODO Add icon and trigger functionality from here --> + <toolbar-button + v-if="supportsQuickActions" + :prepend="true" + tag="/" + :button-title="__('Add a quick action')" + icon="quick-actions" + /> + <comment-templates-dropdown + v-if="newCommentTemplatePath && glFeatures.savedReplies" + :new-comment-template-path="newCommentTemplatePath" + /> + <div class="full-screen"> + <gl-button + v-if="!restrictedToolBarItems.includes('full-screen')" + v-gl-tooltip + class="js-zen-enter" + category="tertiary" + icon="maximize" + size="small" + :title="__('Go full screen')" + :prepend="true" + :aria-label="__('Go full screen')" + /> + </div> + </template> </div> </div> </div> diff --git a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue index 9fd606d775d..dbbcb8a6424 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue @@ -80,11 +80,6 @@ export default { required: false, default: '', }, - quickActionsDocsPath: { - type: String, - required: false, - default: '', - }, drawioEnabled: { type: Boolean, required: false, @@ -245,7 +240,7 @@ export default { :enable-autocomplete="enableAutocomplete" :autocomplete-data-sources="autocompleteDataSources" :markdown-docs-path="markdownDocsPath" - :quick-actions-docs-path="quickActionsDocsPath" + :supports-quick-actions="supportsQuickActions" :show-content-editor-switcher="enableContentEditor" :drawio-enabled="drawioEnabled" :restricted-tool-bar-items="markdownFieldRestrictedToolBarItems" @@ -272,9 +267,10 @@ export default { <content-editor ref="contentEditor" :render-markdown="renderMarkdown" + :markdown-docs-path="markdownDocsPath" :uploads-path="uploadsPath" :markdown="markdown" - :quick-actions-docs-path="quickActionsDocsPath" + :supports-quick-actions="supportsQuickActions" :autofocus="contentEditorAutofocused" :placeholder="formFieldProps.placeholder" :drawio-enabled="drawioEnabled" diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue index 4733afb7504..9cf0d0bafb1 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue @@ -1,24 +1,20 @@ <script> -import { GlButton, GlLink, GlLoadingIcon, GlSprintf, GlIcon } from '@gitlab/ui'; +import { GlButton, GlLoadingIcon, GlSprintf, GlIcon } from '@gitlab/ui'; +import EditorModeSwitcher from './editor_mode_switcher.vue'; export default { components: { GlButton, - GlLink, GlLoadingIcon, GlSprintf, GlIcon, + EditorModeSwitcher, }, props: { markdownDocsPath: { type: String, required: true, }, - quickActionsDocsPath: { - type: String, - required: false, - default: '', - }, canAttachFile: { type: Boolean, required: false, @@ -29,10 +25,20 @@ export default { required: false, default: true, }, + showContentEditorSwitcher: { + type: Boolean, + required: false, + default: false, + }, }, computed: { - hasQuickActionsDocsPath() { - return this.quickActionsDocsPath !== ''; + showEditorModeSwitcher() { + return this.showContentEditorSwitcher && !this.previewMarkdown; + }, + }, + methods: { + handleEditorModeChanged() { + this.$emit('enableContentEditor'); }, }, }; @@ -41,94 +47,76 @@ export default { <template> <div v-if="showCommentToolBar" - class="comment-toolbar gl-mx-2 gl-mb-2 gl-px-4 gl-bg-gray-10 gl-rounded-bottom-left-base gl-rounded-bottom-right-base clearfix" + class="comment-toolbar gl-display-flex gl-flex-direction-row gl-justify-content-space-between gl-mx-2 gl-mb-2 gl-px-2 gl-rounded-bottom-left-base gl-rounded-bottom-right-base" + :class="{ 'gl-bg-gray-10': showContentEditorSwitcher }" > - <div class="toolbar-text gl-font-sm"> - <template v-if="!hasQuickActionsDocsPath && markdownDocsPath"> - <gl-sprintf - :message=" - s__('MarkdownToolbar|Supports %{markdownDocsLinkStart}Markdown%{markdownDocsLinkEnd}') - " - > - <template #markdownDocsLink="{ content }"> - <gl-link :href="markdownDocsPath" target="_blank" class="gl-font-sm">{{ - content - }}</gl-link> - </template> - </gl-sprintf> - </template> - <template v-if="hasQuickActionsDocsPath && markdownDocsPath"> - <gl-sprintf - :message=" - s__( - 'NoteToolbar|Supports %{markdownDocsLinkStart}Markdown%{markdownDocsLinkEnd}. For %{quickActionsDocsLinkStart}quick actions%{quickActionsDocsLinkEnd}, type %{keyboardStart}/%{keyboardEnd}.', - ) - " - > - <template #markdownDocsLink="{ content }"> - <gl-link :href="markdownDocsPath" target="_blank" class="gl-font-sm">{{ - content - }}</gl-link> - </template> - <template #keyboard="{ content }"> - <kbd>{{ content }}</kbd> - </template> - <template #quickActionsDocsLink="{ content }"> - <gl-link :href="quickActionsDocsPath" target="_blank" class="gl-font-sm">{{ - content - }}</gl-link> - </template> - </gl-sprintf> - </template> - </div> - <span v-if="canAttachFile" class="uploading-container gl-font-sm gl-line-height-32"> - <span class="uploading-progress-container hide"> - <gl-icon name="paperclip" /> - <span class="attaching-file-message"></span> - <!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings --> - <span class="uploading-progress">0%</span> - <gl-loading-icon size="sm" inline /> - </span> - <span class="uploading-error-container hide"> - <span class="uploading-error-icon"> + <editor-mode-switcher + v-if="showEditorModeSwitcher" + size="small" + value="markdown" + @input="handleEditorModeChanged" + /> + <div> + <div class="toolbar-text gl-font-sm"> + <template v-if="markdownDocsPath"> + <gl-button + icon="markdown-mark" + :href="markdownDocsPath" + target="_blank" + category="tertiary" + size="small" + /> + </template> + </div> + <span v-if="canAttachFile" class="uploading-container gl-font-sm gl-line-height-32"> + <span class="uploading-progress-container hide"> <gl-icon name="paperclip" /> + <span class="attaching-file-message"></span> + <!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings --> + <span class="uploading-progress">0%</span> + <gl-loading-icon size="sm" inline /> </span> - <span class="uploading-error-message"></span> + <span class="uploading-error-container hide"> + <span class="uploading-error-icon"> + <gl-icon name="paperclip" /> + </span> + <span class="uploading-error-message"></span> - <gl-sprintf - :message=" - __( - '%{retryButtonStart}Try again%{retryButtonEnd} or %{newFileButtonStart}attach a new file%{newFileButtonEnd}.', - ) - " + <gl-sprintf + :message=" + __( + '%{retryButtonStart}Try again%{retryButtonEnd} or %{newFileButtonStart}attach a new file%{newFileButtonEnd}.', + ) + " + > + <template #retryButton="{ content }"> + <gl-button + variant="link" + category="primary" + class="retry-uploading-link gl-vertical-align-baseline gl-font-sm!" + > + {{ content }} + </gl-button> + </template> + <template #newFileButton="{ content }"> + <gl-button + variant="link" + category="primary" + class="markdown-selector attach-new-file gl-vertical-align-baseline gl-font-sm!" + > + {{ content }} + </gl-button> + </template> + </gl-sprintf> + </span> + <gl-button + variant="link" + category="primary" + class="button-cancel-uploading-files gl-vertical-align-baseline hide gl-font-sm!" > - <template #retryButton="{ content }"> - <gl-button - variant="link" - category="primary" - class="retry-uploading-link gl-vertical-align-baseline gl-font-sm!" - > - {{ content }} - </gl-button> - </template> - <template #newFileButton="{ content }"> - <gl-button - variant="link" - category="primary" - class="markdown-selector attach-new-file gl-vertical-align-baseline gl-font-sm!" - > - {{ content }} - </gl-button> - </template> - </gl-sprintf> + {{ __('Cancel') }} + </gl-button> </span> - <gl-button - variant="link" - category="primary" - class="button-cancel-uploading-files gl-vertical-align-baseline hide gl-font-sm!" - > - {{ __('Cancel') }} - </gl-button> - </span> + </div> </div> </template> diff --git a/app/assets/javascripts/work_items/components/work_item_description.vue b/app/assets/javascripts/work_items/components/work_item_description.vue index 7e062776f98..58bf524f450 100644 --- a/app/assets/javascripts/work_items/components/work_item_description.vue +++ b/app/assets/javascripts/work_items/components/work_item_description.vue @@ -36,8 +36,7 @@ export default { required: true, }, }, - markdownDocsPath: helpPagePath('user/project/quick_actions'), - quickActionsDocsPath: helpPagePath('user/project/quick_actions'), + markdownDocsPath: helpPagePath('user/markdown'), data() { return { workItem: {}, diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 322363d7f4b..c7976e11f53 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -348,10 +348,6 @@ table { .toolbar-text { font-size: 14px; line-height: $gl-spacing-scale-7; - - @include media-breakpoint-up(md) { - float: left; - } } .note-form-actions { diff --git a/app/controllers/concerns/redirects_for_missing_path_on_tree.rb b/app/controllers/concerns/redirects_for_missing_path_on_tree.rb index 92574dfade9..97c23a2cf3c 100644 --- a/app/controllers/concerns/redirects_for_missing_path_on_tree.rb +++ b/app/controllers/concerns/redirects_for_missing_path_on_tree.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true module RedirectsForMissingPathOnTree - def redirect_to_tree_root_for_missing_path(project, ref, path) - redirect_to project_tree_path(project, ref), notice: missing_path_on_ref(path, ref) + def redirect_to_tree_root_for_missing_path(project, ref, path, ref_type: nil) + redirect_to project_tree_path(project, ref, ref_type: ref_type), notice: missing_path_on_ref(path, ref) end private diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index c933b05e0c4..196fadb888d 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -129,7 +129,7 @@ class Import::BitbucketController < Import::BaseController end def options - OmniAuth::Strategies::Bitbucket.default_options[:client_options].deep_symbolize_keys + OmniAuth::Strategies::Bitbucket.default_options[:client_options].to_h.deep_symbolize_keys end def verify_bitbucket_import_enabled diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 28393e1f365..a60cc5301e2 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -160,6 +160,8 @@ class Projects::BlobController < Projects::ApplicationController end def check_for_ambiguous_ref + return if Feature.enabled?(:redirect_with_ref_type, @project) + @ref_type = ref_type if @ref_type == ExtractsRef::BRANCH_REF_TYPE && ambiguous_ref?(@project, @ref) @@ -169,7 +171,17 @@ class Projects::BlobController < Projects::ApplicationController end def commit - @commit ||= @repository.commit(@ref) + if Feature.enabled?(:redirect_with_ref_type, @project) + response = ::ExtractsRef::RequestedRef.new(@repository, ref_type: ref_type, ref: @ref).find + @commit = response[:commit] + @ref_type = response[:ref_type] + + if response[:ambiguous] + return redirect_to(project_blob_path(@project, File.join(@ref_type ? @ref : @commit.id, @path), ref_type: @ref_type)) + end + else + @commit ||= @repository.commit(@ref) + end return render_404 unless @commit end diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb index c8f698d6193..d2a820d93b0 100644 --- a/app/controllers/projects/tree_controller.rb +++ b/app/controllers/projects/tree_controller.rb @@ -12,6 +12,7 @@ class Projects::TreeController < Projects::ApplicationController before_action :require_non_empty_project, except: [:new, :create] before_action :assign_ref_vars + before_action :find_requested_ref, only: [:show] before_action :assign_dir_vars, only: [:create_dir] before_action :authorize_read_code! before_action :authorize_edit_tree!, only: [:create_dir] @@ -28,18 +29,20 @@ class Projects::TreeController < Projects::ApplicationController def show return render_404 unless @commit - @ref_type = ref_type - if @ref_type == BRANCH_REF_TYPE && ambiguous_ref?(@project, @ref) - branch = @project.repository.find_branch(@ref) - if branch - redirect_to project_tree_path(@project, branch.target) - return + unless Feature.enabled?(:redirect_with_ref_type, @project) + @ref_type = ref_type + if @ref_type == BRANCH_REF_TYPE && ambiguous_ref?(@project, @ref) + branch = @project.repository.find_branch(@ref) + if branch + redirect_to project_tree_path(@project, branch.target) + return + end end end if tree.entries.empty? if @repository.blob_at(@commit.id, @path) - redirect_to project_blob_path(@project, File.join(@ref, @path)) + redirect_to project_blob_path(@project, File.join(@ref, @path), ref_type: @ref_type) elsif @path.present? redirect_to_tree_root_for_missing_path(@project, @ref, @path) end @@ -59,6 +62,23 @@ class Projects::TreeController < Projects::ApplicationController private + def find_requested_ref + return unless Feature.enabled?(:redirect_with_ref_type, @project) + + @ref_type = ref_type + if @ref_type.present? + @tree = @repo.tree(@ref, @path, ref_type: @ref_type) + else + response = ExtractsPath::RequestedRef.new(@repository, ref_type: nil, ref: @ref).find + @ref_type = response[:ref_type] + @commit = response[:commit] + + if response[:ambiguous] + redirect_to(project_tree_path(@project, File.join(@ref_type ? @ref : @commit.id, @path), ref_type: @ref_type)) + end + end + end + def redirect_renamed_default_branch? action_name == 'show' end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 71a34a40bd0..bc4831d7772 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -173,7 +173,9 @@ class ProjectsController < Projects::ApplicationController flash.now[:alert] = _("Project '%{project_name}' queued for deletion.") % { project_name: @project.name } end - if ambiguous_ref?(@project, @ref) + if Feature.enabled?(:redirect_with_ref_type, @project) + @ref_type = 'heads' + elsif ambiguous_ref?(@project, @ref) branch = @project.repository.find_branch(@ref) # The files view would render a ref other than the default branch diff --git a/app/models/ci/external_pull_request.rb b/app/models/ci/external_pull_request.rb new file mode 100644 index 00000000000..bd37aa9f85a --- /dev/null +++ b/app/models/ci/external_pull_request.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +# This model stores pull requests coming from external providers, such as +# GitHub, when GitLab project is set as CI/CD only and remote mirror. +# +# When setting up a remote mirror with GitHub we subscribe to push and +# pull_request webhook events. When a pull request is opened on GitHub, +# a webhook is sent out, we create or update the status of the pull +# request locally. +# +# When the mirror is updated and changes are pushed to branches we check +# if there are open pull requests for the source and target branch. +# If so, we create pipelines for external pull requests. +module Ci + class ExternalPullRequest < Ci::ApplicationRecord + include Gitlab::Utils::StrongMemoize + include ShaAttribute + include EachBatch + + belongs_to :project + + sha_attribute :source_sha + sha_attribute :target_sha + + validates :source_branch, presence: true + validates :target_branch, presence: true + validates :source_sha, presence: true + validates :target_sha, presence: true + validates :source_repository, presence: true + validates :target_repository, presence: true + validates :status, presence: true + + enum status: { + open: 1, + closed: 2 + } + + # We currently don't support pull requests from fork, so + # we are going to return an error to the webhook + validate :not_from_fork + + scope :by_source_branch, ->(branch) { where(source_branch: branch) } + scope :by_source_repository, ->(repository) { where(source_repository: repository) } + + # Needed to override Ci::ApplicationRecord as this does not have ci_ table prefix + self.table_name = 'external_pull_requests' + + def self.create_or_update_from_params(params) + find_params = params.slice(:project_id, :source_branch, :target_branch) + + safe_find_or_initialize_and_update(find: find_params, update: params) do |pull_request| + yield(pull_request) if block_given? + end + end + + def actual_branch_head? + actual_source_branch_sha == source_sha + end + + def from_fork? + source_repository != target_repository + end + + def source_ref + Gitlab::Git::BRANCH_REF_PREFIX + source_branch + end + + def predefined_variables + Gitlab::Ci::Variables::Collection.new.tap do |variables| + variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_IID', value: pull_request_iid.to_s) + variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_REPOSITORY', value: source_repository) + variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_REPOSITORY', value: target_repository) + variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_SHA', value: source_sha) + variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_SHA', value: target_sha) + variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME', value: source_branch) + variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME', value: target_branch) + end + end + + def modified_paths + project.repository.diff_stats(target_sha, source_sha).paths + end + + private + + def actual_source_branch_sha + project.commit(source_ref)&.sha + end + + def not_from_fork + return unless from_fork? + + errors.add(:base, _('Pull requests from fork are not supported')) + end + + def self.safe_find_or_initialize_and_update(find:, update:) + safe_ensure_unique(retries: 1) do + model = find_or_initialize_by(find) + + yield(model) if model.update(update) && block_given? + + model + end + end + end +end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 21c58e47a06..69e48cadf97 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -51,7 +51,7 @@ module Ci belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline', inverse_of: :auto_canceled_pipelines belongs_to :pipeline_schedule, class_name: 'Ci::PipelineSchedule' belongs_to :merge_request, class_name: 'MergeRequest' - belongs_to :external_pull_request + belongs_to :external_pull_request, class_name: 'Ci::ExternalPullRequest' belongs_to :ci_ref, class_name: 'Ci::Ref', foreign_key: :ci_ref_id, inverse_of: :pipelines has_internal_id :iid, scope: :project, presence: false, diff --git a/app/models/external_pull_request.rb b/app/models/external_pull_request.rb deleted file mode 100644 index 94c242782c1..00000000000 --- a/app/models/external_pull_request.rb +++ /dev/null @@ -1,106 +0,0 @@ -# frozen_string_literal: true - -# This model stores pull requests coming from external providers, such as -# GitHub, when GitLab project is set as CI/CD only and remote mirror. -# -# When setting up a remote mirror with GitHub we subscribe to push and -# pull_request webhook events. When a pull request is opened on GitHub, -# a webhook is sent out, we create or update the status of the pull -# request locally. -# -# When the mirror is updated and changes are pushed to branches we check -# if there are open pull requests for the source and target branch. -# If so, we create pipelines for external pull requests. -class ExternalPullRequest < Ci::ApplicationRecord - include Gitlab::Utils::StrongMemoize - include ShaAttribute - include EachBatch - - belongs_to :project - - sha_attribute :source_sha - sha_attribute :target_sha - - validates :source_branch, presence: true - validates :target_branch, presence: true - validates :source_sha, presence: true - validates :target_sha, presence: true - validates :source_repository, presence: true - validates :target_repository, presence: true - validates :status, presence: true - - enum status: { - open: 1, - closed: 2 - } - - # We currently don't support pull requests from fork, so - # we are going to return an error to the webhook - validate :not_from_fork - - scope :by_source_branch, ->(branch) { where(source_branch: branch) } - scope :by_source_repository, -> (repository) { where(source_repository: repository) } - - # Needed to override Ci::ApplicationRecord as this does not have ci_ table prefix - self.table_name = 'external_pull_requests' - - def self.create_or_update_from_params(params) - find_params = params.slice(:project_id, :source_branch, :target_branch) - - safe_find_or_initialize_and_update(find: find_params, update: params) do |pull_request| - yield(pull_request) if block_given? - end - end - - def actual_branch_head? - actual_source_branch_sha == source_sha - end - - def from_fork? - source_repository != target_repository - end - - def source_ref - Gitlab::Git::BRANCH_REF_PREFIX + source_branch - end - - def predefined_variables - Gitlab::Ci::Variables::Collection.new.tap do |variables| - variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_IID', value: pull_request_iid.to_s) - variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_REPOSITORY', value: source_repository) - variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_REPOSITORY', value: target_repository) - variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_SHA', value: source_sha) - variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_SHA', value: target_sha) - variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME', value: source_branch) - variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME', value: target_branch) - end - end - - def modified_paths - project.repository.diff_stats(target_sha, source_sha).paths - end - - private - - def actual_source_branch_sha - project.commit(source_ref)&.sha - end - - def not_from_fork - if from_fork? - errors.add(:base, _('Pull requests from fork are not supported')) - end - end - - def self.safe_find_or_initialize_and_update(find:, update:) - safe_ensure_unique(retries: 1) do - model = find_or_initialize_by(find) - - if model.update(update) - yield(model) if block_given? - end - - model - end - end -end diff --git a/app/models/integrations/hangouts_chat.rb b/app/models/integrations/hangouts_chat.rb index ad82f1b916f..7ba9bbc38e6 100644 --- a/app/models/integrations/hangouts_chat.rb +++ b/app/models/integrations/hangouts_chat.rb @@ -2,6 +2,23 @@ module Integrations class HangoutsChat < BaseChatNotification + undef :notify_only_broken_pipelines + + field :webhook, + section: SECTION_TYPE_CONNECTION, + help: 'https://chat.googleapis.com/v1/spaces…', + required: true + + field :notify_only_broken_pipelines, + type: 'checkbox', + section: SECTION_TYPE_CONFIGURATION + + field :branches_to_be_notified, + type: 'select', + section: SECTION_TYPE_CONFIGURATION, + title: -> { s_('Integrations|Branches for which notifications are to be sent') }, + choices: -> { branch_choices } + def title 'Google Chat' end @@ -19,25 +36,15 @@ module Integrations s_('Before enabling this integration, create a webhook for the room in Google Chat where you want to receive notifications from this project. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } end - def default_channel_placeholder + def fields + self.class.fields + build_event_channels end - def self.supported_events - %w[push issue confidential_issue merge_request note confidential_note tag_push - pipeline wiki_page] + def default_channel_placeholder end - def default_fields - [ - { type: 'text', name: 'webhook', help: 'https://chat.googleapis.com/v1/spaces…' }, - { type: 'checkbox', name: 'notify_only_broken_pipelines' }, - { - type: 'select', - name: 'branches_to_be_notified', - title: s_('Integrations|Branches for which notifications are to be sent'), - choices: self.class.branch_choices - } - ] + def self.supported_events + %w[push issue confidential_issue merge_request note confidential_note tag_push pipeline wiki_page] end private diff --git a/app/models/project.rb b/app/models/project.rb index 23eb58c6020..e4d8830ec48 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -415,7 +415,7 @@ class Project < ApplicationRecord has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting', inverse_of: :project, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :remote_mirrors, inverse_of: :project - has_many :external_pull_requests, inverse_of: :project + has_many :external_pull_requests, inverse_of: :project, class_name: 'Ci::ExternalPullRequest' has_many :sourced_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_project_id has_many :source_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :project_id diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index a8da83e84a1..fe0e842f542 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -40,22 +40,22 @@ module Ci # Create a new pipeline in the specified project. # - # @param [Symbol] source What event (Ci::Pipeline.sources) triggers the pipeline - # creation. - # @param [Boolean] ignore_skip_ci Whether skipping a pipeline creation when `[skip ci]` comment - # is present in the commit body - # @param [Boolean] save_on_errors Whether persisting an invalid pipeline when it encounters an - # error during creation (e.g. invalid yaml) - # @param [Ci::TriggerRequest] trigger_request The pipeline trigger triggers the pipeline creation. - # @param [Ci::PipelineSchedule] schedule The pipeline schedule triggers the pipeline creation. - # @param [MergeRequest] merge_request The merge request triggers the pipeline creation. - # @param [ExternalPullRequest] external_pull_request The external pull request triggers the pipeline creation. - # @param [Ci::Bridge] bridge The bridge job that triggers the downstream pipeline creation. - # @param [String] content The content of .gitlab-ci.yml to override the default config - # contents (e.g. .gitlab-ci.yml in repostiry). Mainly used for - # generating a dangling pipeline. + # @param [Symbol] source What event (Ci::Pipeline.sources) triggers the pipeline + # creation. + # @param [Boolean] ignore_skip_ci Whether skipping a pipeline creation when `[skip ci]` comment + # is present in the commit body + # @param [Boolean] save_on_errors Whether persisting an invalid pipeline when it encounters an + # error during creation (e.g. invalid yaml) + # @param [Ci::TriggerRequest] trigger_request The pipeline trigger triggers the pipeline creation. + # @param [Ci::PipelineSchedule] schedule The pipeline schedule triggers the pipeline creation. + # @param [MergeRequest] merge_request The merge request triggers the pipeline creation. + # @param [Ci::ExternalPullRequest] external_pull_request The external pull request triggers the pipeline creation. + # @param [Ci::Bridge] bridge The bridge job that triggers the downstream pipeline creation. + # @param [String] content The content of .gitlab-ci.yml to override the default config + # contents (e.g. .gitlab-ci.yml in repostiry). Mainly used for + # generating a dangling pipeline. # - # @return [Ci::Pipeline] The created Ci::Pipeline object. + # @return [Ci::Pipeline] The created Ci::Pipeline object. # rubocop: disable Metrics/ParameterLists def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil, external_pull_request: nil, bridge: nil, **options, &block) @logger = build_logger diff --git a/app/views/shared/_md_preview.html.haml b/app/views/shared/_md_preview.html.haml index dd3a31f5a59..e4fc3ebf23c 100644 --- a/app/views/shared/_md_preview.html.haml +++ b/app/views/shared/_md_preview.html.haml @@ -1,4 +1,5 @@ - referenced_users = local_assigns.fetch(:referenced_users, nil) +- supports_quick_actions = local_assigns.fetch(:supports_quick_actions, false) - if @merge_request&.discussion_locked? .issuable-note-warning @@ -10,12 +11,12 @@ .md-area.position-relative .md-header.gl-bg-gray-50.gl-px-2.gl-rounded-base.gl-mx-2.gl-mt-2 .gl-display-flex.gl-align-items-center.gl-flex-wrap.gl-justify-content-space-between - .md-header-toolbar.gl-display-flex.gl-py-2.gl-flex-wrap - = render 'shared/blob/markdown_buttons' - .switch-preview.gl-py-2.gl-display-flex.gl-align-items-center.gl-ml-auto + .md-header-toolbar.gl-display-flex.gl-py-2.gl-flex-wrap.gl-row-gap-3 = render Pajamas::ButtonComponent.new(category: :tertiary, size: :small, button_options: { class: 'js-md-preview-button', value: 'preview' }) do = _('Preview') - = render Pajamas::ButtonComponent.new(icon: 'maximize', category: :tertiary, size: :small, button_options: { 'tabindex': -1, 'aria-label': _("Go full screen"), class: 'has-tooltip js-zen-enter gl-ml-2', data: { container: 'body' } }) + = render 'shared/blob/markdown_buttons', supports_quick_actions: supports_quick_actions + .full-screen + = render Pajamas::ButtonComponent.new(icon: 'maximize', category: :tertiary, size: :small, button_options: { 'tabindex': -1, 'aria-label': _("Go full screen"), class: 'has-tooltip js-zen-enter', data: { container: 'body' } }) .md-write-holder = yield diff --git a/app/views/shared/blob/_markdown_buttons.html.haml b/app/views/shared/blob/_markdown_buttons.html.haml index a3d3c1c8231..16bffaca810 100644 --- a/app/views/shared/blob/_markdown_buttons.html.haml +++ b/app/views/shared/blob/_markdown_buttons.html.haml @@ -1,32 +1,33 @@ - modifier_key = client_js_flags[:isMac] ? '⌘' : s_('KeyboardKey|Ctrl+') - supports_file_upload = local_assigns.fetch(:supports_file_upload, true) +- supports_quick_actions = local_assigns.fetch(:supports_quick_actions, false) = markdown_toolbar_button({ icon: "bold", - css_class: 'gl-mr-3', + css_class: 'haml-markdown-button gl-mr-3', data: { "md-tag" => "**", "md-shortcuts": '["mod+b"]' }, title: sprintf(s_("MarkdownEditor|Add bold text (%{modifier_key}B)") % { modifier_key: modifier_key }) }) = markdown_toolbar_button({ icon: "italic", - css_class: 'gl-mr-3', + css_class: 'haml-markdown-button gl-mr-3', data: { "md-tag" => "_", "md-shortcuts": '["mod+i"]' }, title: sprintf(s_("MarkdownEditor|Add italic text (%{modifier_key}I)") % { modifier_key: modifier_key }) }) = markdown_toolbar_button({ icon: "strikethrough", - css_class: 'gl-mr-3', + css_class: 'haml-markdown-button gl-mr-3', data: { "md-tag" => "~~", "md-shortcuts": '["mod+shift+x"]' }, title: sprintf(s_("MarkdownEditor|Add strikethrough text (%{modifier_key}⇧X)") % { modifier_key: modifier_key }) }) -= markdown_toolbar_button({ icon: "quote", css_class: 'gl-mr-3', data: { "md-tag" => "> ", "md-prepend" => true }, title: _("Insert a quote") }) -= markdown_toolbar_button({ icon: "code", css_class: 'gl-mr-3', data: { "md-tag" => "`", "md-block" => "```" }, title: _("Insert code") }) += markdown_toolbar_button({ icon: "quote", css_class: 'haml-markdown-button gl-mr-3', data: { "md-tag" => "> ", "md-prepend" => true }, title: _("Insert a quote") }) += markdown_toolbar_button({ icon: "code", css_class: 'haml-markdown-button gl-mr-3', data: { "md-tag" => "`", "md-block" => "```" }, title: _("Insert code") }) = markdown_toolbar_button({ icon: "link", - css_class: 'gl-mr-3', + css_class: 'haml-markdown-button gl-mr-3', data: { "md-tag" => "[{text}](url)", "md-select" => "url", "md-shortcuts": '["mod+k"]' }, title: sprintf(s_("MarkdownEditor|Add a link (%{modifier_key}K)") % { modifier_key: modifier_key }) }) -= markdown_toolbar_button({ icon: "list-bulleted", css_class: 'gl-mr-3', data: { "md-tag" => "- ", "md-prepend" => true }, title: _("Add a bullet list") }) -= markdown_toolbar_button({ icon: "list-numbered", css_class: 'gl-mr-3', data: { "md-tag" => "1. ", "md-prepend" => true }, title: _("Add a numbered list") }) -= markdown_toolbar_button({ icon: "list-task", css_class: 'gl-mr-3', data: { "md-tag" => "- [ ] ", "md-prepend" => true }, title: _("Add a checklist") }) += markdown_toolbar_button({ icon: "list-bulleted", css_class: 'haml-markdown-button gl-mr-3', data: { "md-tag" => "- ", "md-prepend" => true }, title: _("Add a bullet list") }) += markdown_toolbar_button({ icon: "list-numbered", css_class: 'haml-markdown-button gl-mr-3', data: { "md-tag" => "1. ", "md-prepend" => true }, title: _("Add a numbered list") }) += markdown_toolbar_button({ icon: "list-task", css_class: 'haml-markdown-button gl-mr-3', data: { "md-tag" => "- [ ] ", "md-prepend" => true }, title: _("Add a checklist") }) = markdown_toolbar_button({ icon: "list-indent", css_class: 'gl-display-none gl-mr-3', data: { "md-command" => 'indentLines', "md-shortcuts": '["mod+]"]' }, @@ -36,9 +37,11 @@ data: { "md-command" => 'outdentLines', "md-shortcuts": '["mod+["]' }, title: sprintf(s_("MarkdownEditor|Outdent line (%{modifier_key}[)") % { modifier_key: modifier_key }) }) = markdown_toolbar_button({ icon: "details-block", - css_class: 'gl-mr-3', + css_class: 'haml-markdown-button gl-mr-3', data: { "md-tag" => "<details><summary>Click to expand</summary>\n{text}\n</details>", "md-prepend" => true, "md-select" => "Click to expand" }, title: _("Add a collapsible section") }) -= markdown_toolbar_button({ icon: "table", css_class: 'gl-mr-3', data: { "md-tag" => "| header | header |\n| ------ | ------ |\n| | |\n| | |", "md-prepend" => true }, title: _("Add a table") }) += markdown_toolbar_button({ icon: "table", css_class: 'haml-markdown-button gl-mr-3', data: { "md-tag" => "| header | header |\n| ------ | ------ |\n| | |\n| | |", "md-prepend" => true }, title: _("Add a table") }) - if supports_file_upload - = render Pajamas::ButtonComponent.new(icon: 'paperclip', category: :tertiary, size: :small, button_options: { 'aria-label': _("Attach a file or image"), class: 'has-tooltip js-attach-file-button gl-mr-3', data: { testid: 'button-attach-file', container: 'body' } }) + = render Pajamas::ButtonComponent.new(icon: 'paperclip', category: :tertiary, size: :small, button_options: { 'aria-label': _("Attach a file or image"), class: 'has-tooltip js-attach-file-button haml-markdown-button gl-mr-3', data: { testid: 'button-attach-file', container: 'body' } }) +- if supports_quick_actions + = markdown_toolbar_button({ icon: "quick-actions", css_class: 'haml-markdown-button gl-mr-3', data: { "md-tag" => "/", "md-prepend" => true }, title: _("Add a quick action") }) diff --git a/app/views/shared/notes/_form.html.haml b/app/views/shared/notes/_form.html.haml index 98008fede90..6f4b35266c2 100644 --- a/app/views/shared/notes/_form.html.haml +++ b/app/views/shared/notes/_form.html.haml @@ -25,14 +25,14 @@ = f.hidden_field :position .discussion-form-container.discussion-with-resolve-btn.flex-column.p-0 - = render layout: 'shared/md_preview', locals: { url: preview_url, referenced_users: true } do + = render layout: 'shared/md_preview', locals: { url: preview_url, referenced_users: true, supports_quick_actions: supports_quick_actions } do = render 'shared/zen', f: f, qa_selector: 'note_field', attr: :note, classes: 'note-textarea js-note-text', placeholder: _("Write a comment or drag your files here…"), supports_quick_actions: supports_quick_actions, supports_autocomplete: supports_autocomplete - = render 'shared/notes/hints', supports_quick_actions: supports_quick_actions + = render 'shared/notes/hints' .error-alert .note-form-actions.clearfix.gl-display-flex.gl-flex-wrap diff --git a/app/views/shared/notes/_hints.html.haml b/app/views/shared/notes/_hints.html.haml index d7d6e477ab1..9be87b0a095 100644 --- a/app/views/shared/notes/_hints.html.haml +++ b/app/views/shared/notes/_hints.html.haml @@ -1,13 +1,7 @@ -- supports_quick_actions = local_assigns.fetch(:supports_quick_actions, false) - supports_file_upload = local_assigns.fetch(:supports_file_upload, true) -.comment-toolbar.gl-mx-2.gl-mb-2.gl-px-4.gl-bg-gray-10.gl-rounded-bottom-left-base.gl-rounded-bottom-right-base.clearfix - .toolbar-text.gl-font-sm - - markdownLinkStart = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/markdown') } - - quickActionsLinkStart = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/quick_actions') } - - if supports_quick_actions - = html_escape(s_('NoteToolbar|Supports %{markdownDocsLinkStart}Markdown%{markdownDocsLinkEnd}. For %{quickActionsDocsLinkStart}quick actions%{quickActionsDocsLinkEnd}, type %{keyboardStart}/%{keyboardEnd}.')) % { markdownDocsLinkStart: markdownLinkStart, markdownDocsLinkEnd: '</a>'.html_safe, quickActionsDocsLinkStart: quickActionsLinkStart, quickActionsDocsLinkEnd: '</a>'.html_safe, keyboardStart: '<kbd>'.html_safe, keyboardEnd: '</kbd>'.html_safe } - - else - = html_escape(s_('MarkdownToolbar|Supports %{markdownDocsLinkStart}Markdown%{markdownDocsLinkEnd}')) % { markdownDocsLinkStart: markdownLinkStart, markdownDocsLinkEnd: '</a>'.html_safe } +.comment-toolbar.gl-mx-2.gl-mb-2.gl-px-2.gl-display-flex.gl-justify-content-end.gl-rounded-bottom-left-base.gl-rounded-bottom-right-base.clearfix + .toolbar-text + = render Pajamas::ButtonComponent.new(category: :tertiary, icon: 'markdown-mark', size: :small, href: help_page_path('user/markdown'), target: '_blank') - if supports_file_upload %span.uploading-container.gl-line-height-32.gl-font-sm %span.uploading-progress-container.hide diff --git a/config/feature_flags/development/jira_deployment_issue_keys.yml b/config/feature_flags/development/jira_deployment_issue_keys.yml new file mode 100644 index 00000000000..355d789f338 --- /dev/null +++ b/config/feature_flags/development/jira_deployment_issue_keys.yml @@ -0,0 +1,8 @@ +--- +name: jira_deployment_issue_keys +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123455 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/415025 +milestone: '16.2' +type: development +group: group::import and integrate +default_enabled: false diff --git a/config/feature_flags/development/redirect_with_ref_type.yml b/config/feature_flags/development/redirect_with_ref_type.yml new file mode 100644 index 00000000000..74a4d31eb2f --- /dev/null +++ b/config/feature_flags/development/redirect_with_ref_type.yml @@ -0,0 +1,8 @@ +--- +name: redirect_with_ref_type +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122237 +rollout_issue_url: +milestone: '16.2' +type: development +group: group::source code +default_enabled: false diff --git a/doc/development/database/efficient_in_operator_queries.md b/doc/development/database/efficient_in_operator_queries.md index a770dfe6531..03a1c442255 100644 --- a/doc/development/database/efficient_in_operator_queries.md +++ b/doc/development/database/efficient_in_operator_queries.md @@ -672,7 +672,7 @@ end #### Ordering by `JOIN` columns Ordering records by mixed columns where one or more columns are coming from `JOIN` tables -works with limitations. It requires extra configuration (CTE). The trick is to use a +works with limitations. It requires extra configuration via Common Table Expression (CTE). The trick is to use a non-materialized CTE to act as a "fake" table which exposes all required columns. NOTE: diff --git a/doc/integration/jira/development_panel.md b/doc/integration/jira/development_panel.md index 009e620f121..0a780ee61ee 100644 --- a/doc/integration/jira/development_panel.md +++ b/doc/integration/jira/development_panel.md @@ -51,9 +51,9 @@ depends on where you mention the Jira issue ID in GitLab. | GitLab: where you mention the Jira issue ID | Jira development panel: what information is displayed | |------------------------------------------------|-------------------------------------------------------| -| Merge request title or description | Link to the merge request<br>Link to the branch ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/354373) in GitLab 15.11) | -| Branch name | Link to the branch | -| Commit message | Link to the commit | +| Merge request title or description | Link to the merge request<br>Link to the deployment<br>Link to the pipeline by title only and by description ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/390888) in GitLab 15.10)<br>Link to the branch ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/354373) in GitLab 15.11) | +| Branch name | Link to the branch<br>Link to the deployment | +| Commit message | Link to the commit<br>Link to the deployment from up to 5,000 commits after the last successful deployment to the environment ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/300031) in GitLab 16.2 [with a flag](../../administration/feature_flags.md) named `jira_deployment_issue_keys`. Disabled by default) | | [Jira Smart Commit](#jira-smart-commits) | Custom comment, logged time, or workflow transition | ## Jira Smart Commits diff --git a/doc/integration/jira/index.md b/doc/integration/jira/index.md index 2b6395f437b..dbda2e91dee 100644 --- a/doc/integration/jira/index.md +++ b/doc/integration/jira/index.md @@ -46,7 +46,7 @@ This table shows the capabilities available with the Jira issue integration and | [View a list of Jira issues](issues.md#view-jira-issues) | **{check-circle}** Yes | **{dotted-circle}** No | | [Create a Jira issue for a vulnerability](../../user/application_security/vulnerabilities/index.md#create-a-jira-issue-for-a-vulnerability) | **{check-circle}** Yes | **{dotted-circle}** No | | Create a GitLab branch from a Jira issue | **{dotted-circle}** No | **{check-circle}** Yes, in the issue's development panel | -| Mention a Jira issue ID in a GitLab merge request, and deployments are synced | **{dotted-circle}** No | **{check-circle}** Yes, in the issue's development panel | +| Sync GitLab deployments to Jira issues | **{dotted-circle}** No | **{check-circle}** Yes, in the issue's development panel. Mention a Jira issue ID in a GitLab merge request, branch name, or any of the last 5,000 commits made to the branch after the last successful deployment to the environment | ## Privacy considerations diff --git a/doc/user/clusters/agent/gitops/flux_tutorial.md b/doc/user/clusters/agent/gitops/flux_tutorial.md index c6c9ed9e373..d0780f85201 100644 --- a/doc/user/clusters/agent/gitops/flux_tutorial.md +++ b/doc/user/clusters/agent/gitops/flux_tutorial.md @@ -9,6 +9,9 @@ info: A tutorial using Flux This tutorial teaches you how to set up Flux for GitOps. You'll complete a bootstrap installation, install `agentk` in your cluster, and deploy a simple `nginx` application. +<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> For an overview of an example Flux +configuration, see [Flux bootstrap and manifest synchronization with GitLab](https://www.youtube.com/watch?v=EjPVRM-N_PQ). + To set up Flux for GitOps: 1. [Create a personal access token](#create-a-personal-access-token) diff --git a/doc/user/clusters/agent/index.md b/doc/user/clusters/agent/index.md index 8b1a55bc7bd..1c999e019b2 100644 --- a/doc/user/clusters/agent/index.md +++ b/doc/user/clusters/agent/index.md @@ -55,7 +55,6 @@ This workflow has a weaker security model and is not recommended for production ## Supported Kubernetes versions for GitLab features GitLab supports the following Kubernetes versions. If you want to run -GitLab in a Kubernetes cluster, you might need a different version of Kubernetes. GitLab in a Kubernetes cluster, you might need [a different version of Kubernetes](https://docs.gitlab.com/charts/installation/cloud/index.html). You can upgrade your Kubernetes version to a supported version at any time: diff --git a/gems/gitlab-rspec/gitlab-rspec.gemspec b/gems/gitlab-rspec/gitlab-rspec.gemspec index f9cc83bb497..061647190ec 100644 --- a/gems/gitlab-rspec/gitlab-rspec.gemspec +++ b/gems/gitlab-rspec/gitlab-rspec.gemspec @@ -12,7 +12,7 @@ Gem::Specification.new do |spec| spec.description = "A set of useful helpers to configure RSpec with various stubs and CI configs." spec.homepage = "https://gitlab.com/gitlab-org/gitlab/-/tree/master/gems/gitlab-rspec" spec.license = "MIT" - spec.required_ruby_version = ">= 2.7" + spec.required_ruby_version = ">= 3.0" spec.files = Dir['lib/**/*.rb'] spec.test_files = Dir['spec/**/*'] diff --git a/lib/atlassian/jira_connect/serializers/build_entity.rb b/lib/atlassian/jira_connect/serializers/build_entity.rb index b595d0c2a92..31ab4ece8fe 100644 --- a/lib/atlassian/jira_connect/serializers/build_entity.rb +++ b/lib/atlassian/jira_connect/serializers/build_entity.rb @@ -22,13 +22,7 @@ module Atlassian expose :references def issue_keys - commit_message_issue_keys = JiraIssueKeyExtractor.new(pipeline.git_commit_message).issue_keys - - # extract Jira issue keys from either the source branch/ref or the merge request title. - @issue_keys ||= commit_message_issue_keys + pipeline.all_merge_requests.flat_map do |mr| - src = "#{mr.source_branch} #{mr.title} #{mr.description}" - JiraIssueKeyExtractor.new(src).issue_keys - end.uniq + @issue_keys ||= (pipeline_commit_issue_keys + pipeline_mrs_issue_keys).uniq end private @@ -89,6 +83,18 @@ module Atlassian def update_sequence_id options[:update_sequence_id] || Client.generate_update_sequence_id end + + def pipeline_commit_issue_keys + JiraIssueKeyExtractor.new(pipeline.git_commit_message).issue_keys + end + + # Extract Jira issue keys from either the source branch/ref, merge request title or merge request description. + def pipeline_mrs_issue_keys + pipeline.all_merge_requests.flat_map do |mr| + src = "#{mr.source_branch} #{mr.title} #{mr.description}" + JiraIssueKeyExtractor.new(src).issue_keys + end + end end end end diff --git a/lib/atlassian/jira_connect/serializers/deployment_entity.rb b/lib/atlassian/jira_connect/serializers/deployment_entity.rb index 9ef1666b61c..96e7b1726cb 100644 --- a/lib/atlassian/jira_connect/serializers/deployment_entity.rb +++ b/lib/atlassian/jira_connect/serializers/deployment_entity.rb @@ -6,6 +6,8 @@ module Atlassian class DeploymentEntity < Grape::Entity include Gitlab::Routing + COMMITS_LIMIT = 5_000 + format_with(:iso8601, &:iso8601) expose :schema_version, as: :schemaVersion @@ -22,9 +24,7 @@ module Atlassian expose :environment_entity, as: :environment def issue_keys - return [] unless build&.pipeline.present? - - @issue_keys ||= BuildEntity.new(build.pipeline).issue_keys + @issue_keys ||= (issue_keys_from_pipeline + issue_keys_from_commits_since_last_deploy).uniq end private @@ -74,7 +74,7 @@ module Atlassian end def pipeline_entity - PipelineEntity.new(build.pipeline) if build&.pipeline.present? + PipelineEntity.new(build.pipeline) if pipeline? end def environment_entity @@ -84,6 +84,44 @@ module Atlassian def update_sequence_id options[:update_sequence_id] || Client.generate_update_sequence_id end + + def pipeline? + build&.pipeline.present? + end + + def issue_keys_from_pipeline + return [] unless pipeline? + + BuildEntity.new(build.pipeline).issue_keys + end + + # Extract Jira issue keys from commits made to the deployment's branch or tag + # since the last successful deployment was made to the environment. + def issue_keys_from_commits_since_last_deploy + return [] if Feature.disabled?(:jira_deployment_issue_keys, project) + + last_deployed_commit = environment + .successful_deployments + .id_not_in(deployment.id) + .ordered + .find_by_ref(deployment.ref) + &.commit + + commits = project.repository.commits( + deployment.ref, + before: deployment.commit.created_at, + after: last_deployed_commit&.created_at, + skip_merges: true, + limit: COMMITS_LIMIT + ) + + # Include this deploy's commit, as the `before:` param in `Repository#list_commits_by` excluded it. + commits << deployment.commit + + commits.flat_map do |commit| + JiraIssueKeyExtractor.new(commit.message).issue_keys + end.compact + end end end end diff --git a/lib/bitbucket/connection.rb b/lib/bitbucket/connection.rb index 9937236fc44..64550a0525c 100644 --- a/lib/bitbucket/connection.rb +++ b/lib/bitbucket/connection.rb @@ -63,7 +63,7 @@ module Bitbucket end def options - OmniAuth::Strategies::Bitbucket.default_options[:client_options].deep_symbolize_keys + OmniAuth::Strategies::Bitbucket.default_options[:client_options].to_h.deep_symbolize_keys end end end diff --git a/lib/extracts_ref.rb b/lib/extracts_ref.rb index 49ec564eb8d..2a48b66bb5c 100644 --- a/lib/extracts_ref.rb +++ b/lib/extracts_ref.rb @@ -100,7 +100,7 @@ module ExtractsRef # rubocop:enable Gitlab/ModuleWithInstanceVariables def tree - @tree ||= @repo.tree(@commit.id, @path) # rubocop:disable Gitlab/ModuleWithInstanceVariables + @tree ||= @repo.tree(@commit.id, @path, ref_type: ref_type) # rubocop:disable Gitlab/ModuleWithInstanceVariables end def extract_ref_path diff --git a/lib/extracts_ref/requested_ref.rb b/lib/extracts_ref/requested_ref.rb new file mode 100644 index 00000000000..f20018b5ef4 --- /dev/null +++ b/lib/extracts_ref/requested_ref.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +module ExtractsRef + class RequestedRef + include Gitlab::Utils::StrongMemoize + + SYMBOLIC_REF_PREFIX = %r{((refs/)?(heads|tags)/)+} + def initialize(repository, ref_type:, ref:) + @ref_type = ref_type + @ref = ref + @repository = repository + end + + attr_reader :repository, :ref_type, :ref + + def find + case ref_type + when 'tags' + { ref_type: ref_type, commit: tag } + when 'heads' + { ref_type: ref_type, commit: branch } + else + commit_without_ref_type + end + end + + private + + def commit_without_ref_type + if commit.nil? + { ref_type: nil, commit: nil } + elsif commit.id == ref + # ref is probably complete 40 character sha + { ref_type: nil, commit: commit } + elsif tag.present? + { ref_type: 'tags', commit: tag, ambiguous: branch.present? } + elsif branch.present? + { ref_type: 'heads', commit: branch } + else + { ref_type: nil, commit: commit, ambiguous: ref.match?(SYMBOLIC_REF_PREFIX) } + end + end + + def commit + repository.commit(ref) + end + strong_memoize_attr :commit + + def tag + raw_commit = repository.find_tag(ref)&.dereferenced_target + ::Commit.new(raw_commit, repository.container) if raw_commit + end + strong_memoize_attr :tag + + def branch + raw_commit = repository.find_branch(ref)&.dereferenced_target + ::Commit.new(raw_commit, repository.container) if raw_commit + end + strong_memoize_attr :branch + end +end diff --git a/lib/gitlab/ci/project_config/repository.rb b/lib/gitlab/ci/project_config/repository.rb index 7dfd528fd6f..a08cf27b74c 100644 --- a/lib/gitlab/ci/project_config/repository.rb +++ b/lib/gitlab/ci/project_config/repository.rb @@ -4,6 +4,8 @@ module Gitlab module Ci class ProjectConfig class Repository < Source + extend ::Gitlab::Utils::Override + def content strong_memoize(:content) do next unless file_in_repository? diff --git a/lib/gitlab/ci/project_config/source.rb b/lib/gitlab/ci/project_config/source.rb index 68853ca8296..5f37c3bad7b 100644 --- a/lib/gitlab/ci/project_config/source.rb +++ b/lib/gitlab/ci/project_config/source.rb @@ -5,7 +5,6 @@ module Gitlab class ProjectConfig class Source include Gitlab::Utils::StrongMemoize - extend ::Gitlab::Utils::Override def initialize(project, sha, custom_content, pipeline_source, pipeline_source_bridge) @project = project diff --git a/lib/gitlab/import_export/project/relation_factory.rb b/lib/gitlab/import_export/project/relation_factory.rb index 8c673acdd1a..fe9e64a15dd 100644 --- a/lib/gitlab/import_export/project/relation_factory.rb +++ b/lib/gitlab/import_export/project/relation_factory.rb @@ -16,6 +16,8 @@ module Gitlab bridges: 'Ci::Bridge', runners: 'Ci::Runner', pipeline_metadata: 'Ci::PipelineMetadata', + external_pull_request: 'Ci::ExternalPullRequest', + external_pull_requests: 'Ci::ExternalPullRequest', hooks: 'ProjectHook', merge_access_levels: 'ProtectedBranch::MergeAccessLevel', push_access_levels: 'ProtectedBranch::PushAccessLevel', @@ -61,8 +63,7 @@ module Gitlab epic ProjectCiCdSetting container_expiration_policy - external_pull_request - external_pull_requests + Ci::ExternalPullRequest DesignManagement::Design MergeRequest::DiffCommitUser MergeRequestDiffCommit diff --git a/lib/gitlab/legacy_github_import/client.rb b/lib/gitlab/legacy_github_import/client.rb index 16c3bc09c4d..e1c3b09d371 100644 --- a/lib/gitlab/legacy_github_import/client.rb +++ b/lib/gitlab/legacy_github_import/client.rb @@ -116,7 +116,7 @@ module Gitlab if config config["args"]["client_options"].deep_symbolize_keys else - OmniAuth::Strategies::GitHub.default_options[:client_options].symbolize_keys + OmniAuth::Strategies::GitHub.default_options[:client_options].to_h.symbolize_keys end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index e2b72fb0123..f754d328647 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2703,6 +2703,9 @@ msgstr "" msgid "Add a numbered list" msgstr "" +msgid "Add a quick action" +msgstr "" + msgid "Add a related epic" msgstr "" @@ -12518,9 +12521,6 @@ msgstr "" msgid "Content parsed with %{link}." msgstr "" -msgid "ContentEditor|For %{quickActionsDocsLinkStart}quick actions%{quickActionsDocsLinkEnd}, type %{keyboardStart}/%{keyboardEnd}." -msgstr "" - msgid "ContentEditor|You have to provide a renderMarkdown function or a custom serializer" msgstr "" @@ -15134,9 +15134,15 @@ msgstr "" msgid "Dependencies|Packager" msgstr "" +msgid "Dependencies|Projects" +msgstr "" + msgid "Dependencies|Software Bill of Materials (SBOM) based on the %{linkStart}latest successful%{linkEnd} scan" msgstr "" +msgid "Dependencies|Software Bill of Materials (SBOM) based on the latest successful scan of each project." +msgstr "" + msgid "Dependencies|The %{codeStartTag}dependency_scanning%{codeEndTag} job has failed and cannot generate the list. Please ensure the job is running properly and run the pipeline again." msgstr "" @@ -27865,9 +27871,6 @@ msgstr "" msgid "MarkdownEditor|header" msgstr "" -msgid "MarkdownToolbar|Supports %{markdownDocsLinkStart}Markdown%{markdownDocsLinkEnd}" -msgstr "" - msgid "Marked" msgstr "" @@ -30935,9 +30938,6 @@ msgstr "" msgid "NoteForm|Note" msgstr "" -msgid "NoteToolbar|Supports %{markdownDocsLinkStart}Markdown%{markdownDocsLinkEnd}. For %{quickActionsDocsLinkStart}quick actions%{quickActionsDocsLinkEnd}, type %{keyboardStart}/%{keyboardEnd}." -msgstr "" - msgid "Notes" msgstr "" @@ -45029,10 +45029,10 @@ msgstr "" msgid "Switch to GitLab Next" msgstr "" -msgid "Switch to Markdown" +msgid "Switch to plain text editing" msgstr "" -msgid "Switch to rich text" +msgid "Switch to rich text editing" msgstr "" msgid "Switch to the source to copy the file contents" diff --git a/qa/qa/service/praefect_manager.rb b/qa/qa/service/praefect_manager.rb index 684b18a97a7..1c7c0de09fe 100644 --- a/qa/qa/service/praefect_manager.rb +++ b/qa/qa/service/praefect_manager.rb @@ -357,58 +357,6 @@ module QA result.size >= 5 end - def list_untracked_repositories - untracked_repositories = [] - shell "docker exec #{@praefect} bash -c 'gitlab-ctl praefect list-untracked-repositories'" do |line| - # Results look like this - # The following repositories were found on disk, but missing from the tracking database: - # {"relative_path":"@hashed/aa/bb.git","storage":"gitaly1","virtual_storage":"default"} - # {"relative_path":"@hashed/bb/cc.git","storage":"gitaly3","virtual_storage":"default"} - - QA::Runtime::Logger.debug(line.chomp) - untracked_repositories.append(JSON.parse(line)) - rescue JSON::ParserError - # Ignore lines that can't be parsed as JSON - end - - QA::Runtime::Logger.debug("list_untracked_repositories --- #{untracked_repositories}") - untracked_repositories - end - - def track_repository_in_praefect(relative_path, storage, virtual_storage) - cmd = "gitlab-ctl praefect track-repository --repository-relative-path #{relative_path} --authoritative-storage #{storage} --virtual-storage-name #{virtual_storage}" - shell "docker exec #{@praefect} bash -c '#{cmd}'" - end - - def remove_tracked_praefect_repository(relative_path, virtual_storage) - cmd = "gitlab-ctl praefect remove-repository --repository-relative-path #{relative_path} --virtual-storage-name #{virtual_storage} --apply" - shell "docker exec #{@praefect} bash -c '#{cmd}'" - end - - # set_replication_factor assigns or unassigns random storage nodes as necessary to reach the desired replication factor for a repository - def set_replication_factor(relative_path, virtual_storage, factor) - cmd = "/opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml set-replication-factor -repository #{relative_path} -virtual-storage #{virtual_storage} -replication-factor #{factor}" - shell "docker exec #{@praefect} bash -c '#{cmd}'" - end - - # get_replication_storages retrieves a list of currently assigned storages for a repository - def get_replication_storages(relative_path, virtual_storage) - storage_repositories = [] - query = "SELECT storage FROM repository_assignments WHERE relative_path='#{relative_path}' AND virtual_storage='#{virtual_storage}';" - shell(sql_to_docker_exec_cmd(query)) { |line| storage_repositories << line.strip } - # Returned data from query will be in format - # storage - # -------- - # gitaly1 - # gitaly3 - # gitaly2 - # (3 rows) - # - - # remove 2 header rows and last 2 rows from query response (including blank line) - storage_repositories[2..-3] - end - def modify_repo_access_time(node, repo_path, update_time) repo = "/var/opt/gitlab/git-data/repositories/#{repo_path}" shell(%{ @@ -416,65 +364,6 @@ module QA }) end - def add_repo_to_disk(node, repo_path) - cmd = "GIT_DIR=. git init --initial-branch=main /var/opt/gitlab/git-data/repositories/#{repo_path}" - shell "docker exec --user git #{node} bash -c '#{cmd}'" - modify_repo_access_time(node, repo_path, "24 hours ago") - end - - def remove_repo_from_disk(repo_path) - cmd = "rm -rf /var/opt/gitlab/git-data/repositories/#{repo_path}" - shell "docker exec #{@primary_node} bash -c '#{cmd}'" - shell "docker exec #{@secondary_node} bash -c '#{cmd}'" - shell "docker exec #{@tertiary_node} bash -c '#{cmd}'" - end - - def remove_repository_from_praefect_database(relative_path) - shell sql_to_docker_exec_cmd("delete from repositories where relative_path = '#{relative_path}';") - shell sql_to_docker_exec_cmd("delete from storage_repositories where relative_path = '#{relative_path}';") - end - - def praefect_database_tracks_repo?(relative_path) - storage_repositories = [] - shell sql_to_docker_exec_cmd("SELECT count(*) FROM storage_repositories where relative_path='#{relative_path}';") do |line| - storage_repositories << line - end - QA::Runtime::Logger.debug("storage_repositories count is ---#{storage_repositories}") - - repositories = [] - shell sql_to_docker_exec_cmd("SELECT count(*) FROM repositories where relative_path='#{relative_path}';") do |line| - repositories << line - end - QA::Runtime::Logger.debug("repositories count is ---#{repositories}") - - (storage_repositories[2].to_i >= 1) && (repositories[2].to_i >= 1) - end - - def repository_replicated_to_disk?(node, relative_path) - Support::Waiter.wait_until(max_duration: 300, sleep_interval: 1, raise_on_failure: false) do - result = [] - shell sql_to_docker_exec_cmd("SELECT count(*) FROM storage_repositories where relative_path='#{relative_path}';") do |line| - result << line - end - QA::Runtime::Logger.debug("result is ---#{result}") - result[2].to_i == 3 - end - - repository_exists_on_node_disk?(node, relative_path) - end - - def repository_exists_on_node_disk?(node, relative_path) - # If the dir does not exist it has a non zero exit code leading to a error being raised - # Instead we echo a test line if the dir does not exist, which has a zero exit code, with no output - bash_command = "test -d /var/opt/gitlab/git-data/repositories/#{relative_path} || echo -n 'DIR_DOES_NOT_EXIST'" - result = [] - shell "docker exec #{node} bash -c '#{bash_command}'" do |line| - result << line - end - QA::Runtime::Logger.debug("result is ---#{result}") - result.exclude?("DIR_DOES_NOT_EXIST") - end - private def dataloss_command diff --git a/qa/qa/specs/features/api/12_systems/gitaly/praefect_repo_sync_spec.rb b/qa/qa/specs/features/api/12_systems/gitaly/praefect_repo_sync_spec.rb deleted file mode 100644 index 4f916300ee3..00000000000 --- a/qa/qa/specs/features/api/12_systems/gitaly/praefect_repo_sync_spec.rb +++ /dev/null @@ -1,96 +0,0 @@ -# frozen_string_literal: true - -module QA - RSpec.describe 'Systems' do - describe 'Praefect repository commands', :orchestrated, :gitaly_cluster, product_group: :gitaly do - let(:praefect_manager) { Service::PraefectManager.new } - - let(:repo1) do - { "relative_path" => "@hashed/repo1.git", "storage" => "gitaly1", "virtual_storage" => "default" } - end - - let(:repo2) do - { "relative_path" => "@hashed/path/to/repo2.git", "storage" => "gitaly3", "virtual_storage" => "default" } - end - - before do - praefect_manager.start_all_nodes - praefect_manager.add_repo_to_disk(praefect_manager.primary_node, repo1["relative_path"]) - praefect_manager.add_repo_to_disk(praefect_manager.tertiary_node, repo2["relative_path"]) - end - - after do - praefect_manager.remove_repo_from_disk(repo1["relative_path"]) - praefect_manager.remove_repo_from_disk(repo2["relative_path"]) - praefect_manager.remove_repository_from_praefect_database(repo1["relative_path"]) - praefect_manager.remove_repository_from_praefect_database(repo2["relative_path"]) - end - - it 'allows admin to manage difference between praefect database and disk state', - testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347606' do - # Some repos are on disk that praefect is not aware of - untracked_repositories = praefect_manager.list_untracked_repositories - expect(untracked_repositories).to include(repo1) - expect(untracked_repositories).to include(repo2) - - # admin manually adds the first repo to the praefect database - praefect_manager - .track_repository_in_praefect(repo1["relative_path"], repo1["storage"], repo1["virtual_storage"]) - untracked_repositories = praefect_manager.list_untracked_repositories - expect(untracked_repositories).not_to include(repo1) - expect(untracked_repositories).to include(repo2) - expect(praefect_manager.repository_exists_on_node_disk?(praefect_manager.primary_node, repo1["relative_path"])) - .to be true - expect(praefect_manager.praefect_database_tracks_repo?(repo1["relative_path"])).to be true - - # admin manually adds the second repo to the praefect database - praefect_manager - .track_repository_in_praefect(repo2["relative_path"], repo2["storage"], repo2["virtual_storage"]) - untracked_repositories = praefect_manager.list_untracked_repositories - expect(untracked_repositories).not_to include(repo2) - expect(praefect_manager.repository_exists_on_node_disk?(praefect_manager.tertiary_node, repo2["relative_path"])) - .to be true - expect(praefect_manager.praefect_database_tracks_repo?(repo2["relative_path"])).to be true - - # admin ensures replication to other nodes occurs - expect(praefect_manager.repository_replicated_to_disk?(praefect_manager.secondary_node, repo1["relative_path"])) - .to be true - expect(praefect_manager.repository_replicated_to_disk?(praefect_manager.tertiary_node, repo1["relative_path"])) - .to be true - expect(praefect_manager.repository_replicated_to_disk?(praefect_manager.primary_node, repo2["relative_path"])) - .to be true - expect(praefect_manager.repository_replicated_to_disk?(praefect_manager.secondary_node, repo2["relative_path"])) - .to be true - - # admin chooses to remove the first repo completely from praefect and disk - praefect_manager.remove_tracked_praefect_repository(repo1["relative_path"], repo1["virtual_storage"]) - expect(praefect_manager.repository_exists_on_node_disk?(praefect_manager.primary_node, repo1["relative_path"])) - .to be false - expect(praefect_manager.repository_exists_on_node_disk?(praefect_manager - .secondary_node, repo1["relative_path"])).to be false - expect(praefect_manager.repository_exists_on_node_disk?(praefect_manager.tertiary_node, repo1["relative_path"])) - .to be false - expect(praefect_manager.praefect_database_tracks_repo?(repo1["relative_path"])).to be false - - untracked_repositories = praefect_manager.list_untracked_repositories - expect(untracked_repositories).not_to include(repo1) - end - - it 'allows admin to control the number of replicas of data', - testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347566' do - praefect_manager - .track_repository_in_praefect(repo1['relative_path'], repo1['storage'], repo1['virtual_storage']) - - praefect_manager.set_replication_factor(repo1['relative_path'], repo1['virtual_storage'], 2) - replication_storages = praefect_manager - .get_replication_storages(repo1['relative_path'], repo1['virtual_storage']) - expect(replication_storages).to have_attributes(size: 2) - - praefect_manager.set_replication_factor(repo1['relative_path'], repo1['virtual_storage'], 3) - replication_storages = praefect_manager - .get_replication_storages(repo1['relative_path'], repo1['virtual_storage']) - expect(replication_storages).to eq(%w[gitaly1 gitaly2 gitaly3]) - end - end - end -end diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb index 906cc5cb336..34e8e5af1f4 100644 --- a/spec/controllers/import/bitbucket_controller_spec.rb +++ b/spec/controllers/import/bitbucket_controller_spec.rb @@ -31,6 +31,16 @@ RSpec.describe Import::BitbucketController, feature_category: :importers do let(:external_bitbucket_auth_url) { "http://fake.bitbucket.host/url" } it "redirects to external auth url" do + expected_client_options = { + site: OmniAuth::Strategies::Bitbucket.default_options[:client_options]['site'], + authorize_url: OmniAuth::Strategies::Bitbucket.default_options[:client_options]['authorize_url'], + token_url: OmniAuth::Strategies::Bitbucket.default_options[:client_options]['token_url'] + } + + expect(OAuth2::Client) + .to receive(:new) + .with(anything, anything, expected_client_options) + allow(SecureRandom).to receive(:base64).and_return(random_key) allow_next_instance_of(OAuth2::Client) do |client| allow(client).to receive_message_chain(:auth_code, :authorize_url) @@ -101,7 +111,7 @@ RSpec.describe Import::BitbucketController, feature_category: :importers do @invalid_repo = double(name: 'mercurialrepo', slug: 'mercurialrepo', owner: 'asd', full_name: 'asd/mercurialrepo', clone_url: 'http://test.host/demo/mercurialrepo.git', 'valid?' => false) end - context "when token does not exists" do + context "when token does not exist" do let(:random_key) { "pure_random" } let(:external_bitbucket_auth_url) { "http://fake.bitbucket.host/url" } diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb index b07cb7a228d..49c1935c4a3 100644 --- a/spec/controllers/projects/blob_controller_spec.rb +++ b/spec/controllers/projects/blob_controller_spec.rb @@ -5,15 +5,21 @@ require 'spec_helper' RSpec.describe Projects::BlobController, feature_category: :source_code_management do include ProjectForksHelper - let(:project) { create(:project, :public, :repository, previous_default_branch: previous_default_branch) } - let(:previous_default_branch) { nil } + let_it_be(:project) { create(:project, :public, :repository) } describe "GET show" do - let(:params) { { namespace_id: project.namespace, project_id: project, id: id } } + let(:params) { { namespace_id: project.namespace, project_id: project, id: id, ref_type: ref_type } } + let(:ref_type) { nil } let(:request) do get(:show, params: params) end + let(:redirect_with_ref_type) { true } + + before do + stub_feature_flags(redirect_with_ref_type: redirect_with_ref_type) + end + render_views context 'with file path' do @@ -24,25 +30,43 @@ RSpec.describe Projects::BlobController, feature_category: :source_code_manageme request end + after do + project.repository.rm_tag(project.creator, 'ambiguous_ref') + project.repository.rm_branch(project.creator, 'ambiguous_ref') + end + context 'when the ref is ambiguous' do let(:ref) { 'ambiguous_ref' } let(:path) { 'README.md' } let(:id) { "#{ref}/#{path}" } - let(:params) { { namespace_id: project.namespace, project_id: project, id: id, ref_type: ref_type } } - context 'and explicitly requesting a branch' do - let(:ref_type) { 'heads' } + context 'and the redirect_with_ref_type flag is disabled' do + let(:redirect_with_ref_type) { false } + + context 'and explicitly requesting a branch' do + let(:ref_type) { 'heads' } + + it 'redirects to blob#show with sha for the branch' do + expect(response).to redirect_to(project_blob_path(project, "#{RepoHelpers.another_sample_commit.id}/#{path}")) + end + end + + context 'and explicitly requesting a tag' do + let(:ref_type) { 'tags' } - it 'redirects to blob#show with sha for the branch' do - expect(response).to redirect_to(project_blob_path(project, "#{RepoHelpers.another_sample_commit.id}/#{path}")) + it 'responds with success' do + expect(response).to be_ok + end end end - context 'and explicitly requesting a tag' do - let(:ref_type) { 'tags' } + context 'and the redirect_with_ref_type flag is enabled' do + context 'when the ref_type is nil' do + let(:ref_type) { nil } - it 'responds with success' do - expect(response).to be_ok + it 'redirects to the tag' do + expect(response).to redirect_to(project_blob_path(project, id, ref_type: 'tags')) + end end end end @@ -68,18 +92,20 @@ RSpec.describe Projects::BlobController, feature_category: :source_code_manageme it { is_expected.to respond_with(:not_found) } end - context "renamed default branch, valid file" do - let(:id) { 'old-default-branch/README.md' } - let(:previous_default_branch) { 'old-default-branch' } + context 'when default branch was renamed' do + let_it_be_with_reload(:project) { create(:project, :public, :repository, previous_default_branch: 'old-default-branch') } - it { is_expected.to redirect_to("/#{project.full_path}/-/blob/#{project.default_branch}/README.md") } - end + context "renamed default branch, valid file" do + let(:id) { 'old-default-branch/README.md' } + + it { is_expected.to redirect_to("/#{project.full_path}/-/blob/#{project.default_branch}/README.md") } + end - context "renamed default branch, invalid file" do - let(:id) { 'old-default-branch/invalid-path.rb' } - let(:previous_default_branch) { 'old-default-branch' } + context "renamed default branch, invalid file" do + let(:id) { 'old-default-branch/invalid-path.rb' } - it { is_expected.to redirect_to("/#{project.full_path}/-/blob/#{project.default_branch}/invalid-path.rb") } + it { is_expected.to redirect_to("/#{project.full_path}/-/blob/#{project.default_branch}/invalid-path.rb") } + end end context "binary file" do @@ -100,7 +126,7 @@ RSpec.describe Projects::BlobController, feature_category: :source_code_manageme let(:id) { 'master/README.md' } before do - get :show, params: { namespace_id: project.namespace, project_id: project, id: id }, format: :json + get :show, params: params, format: :json end it do @@ -114,7 +140,7 @@ RSpec.describe Projects::BlobController, feature_category: :source_code_manageme let(:id) { 'master/README.md' } before do - get :show, params: { namespace_id: project.namespace, project_id: project, id: id, viewer: 'none' }, format: :json + get :show, params: { namespace_id: project.namespace, project_id: project, id: id, ref_type: 'heads', viewer: 'none' }, format: :json end it do @@ -127,7 +153,7 @@ RSpec.describe Projects::BlobController, feature_category: :source_code_manageme context 'with tree path' do before do - get :show, params: { namespace_id: project.namespace, project_id: project, id: id } + get :show, params: params controller.instance_variable_set(:@blob, nil) end @@ -414,6 +440,10 @@ RSpec.describe Projects::BlobController, feature_category: :source_code_manageme let(:after_delete_path) { project_tree_path(project, 'master/files') } it 'redirects to the sub directory' do + expect_next_instance_of(Files::DeleteService) do |instance| + expect(instance).to receive(:execute).and_return({ status: :success }) + end + delete :destroy, params: default_params expect(response).to redirect_to(after_delete_path) diff --git a/spec/controllers/projects/tree_controller_spec.rb b/spec/controllers/projects/tree_controller_spec.rb index 61998d516e8..ffec670e97d 100644 --- a/spec/controllers/projects/tree_controller_spec.rb +++ b/spec/controllers/projects/tree_controller_spec.rb @@ -3,9 +3,9 @@ require 'spec_helper' RSpec.describe Projects::TreeController, feature_category: :source_code_management do - let(:project) { create(:project, :repository, previous_default_branch: previous_default_branch) } - let(:previous_default_branch) { nil } + let_it_be(:project) { create(:project, :repository) } let(:user) { create(:user) } + let(:redirect_with_ref_type) { true } before do sign_in(user) @@ -17,10 +17,14 @@ RSpec.describe Projects::TreeController, feature_category: :source_code_manageme describe "GET show" do let(:params) do { - namespace_id: project.namespace.to_param, project_id: project, id: id + namespace_id: project.namespace.to_param, project_id: project, id: id, ref_type: ref_type } end + let(:request) { get :show, params: params } + + let(:ref_type) { nil } + # Make sure any errors accessing the tree in our views bubble up to this spec render_views @@ -28,26 +32,79 @@ RSpec.describe Projects::TreeController, feature_category: :source_code_manageme expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original project.repository.add_tag(project.creator, 'ambiguous_ref', RepoHelpers.sample_commit.id) project.repository.add_branch(project.creator, 'ambiguous_ref', RepoHelpers.another_sample_commit.id) - get :show, params: params + + stub_feature_flags(redirect_with_ref_type: redirect_with_ref_type) + end + + after do + project.repository.rm_tag(project.creator, 'ambiguous_ref') + project.repository.rm_branch(project.creator, 'ambiguous_ref') end - context 'when the ref is ambiguous' do - let(:id) { 'ambiguous_ref' } - let(:params) { { namespace_id: project.namespace, project_id: project, id: id, ref_type: ref_type } } + context 'when the redirect_with_ref_type flag is disabled' do + let(:redirect_with_ref_type) { false } - context 'and explicitly requesting a branch' do - let(:ref_type) { 'heads' } + context 'when there is a ref and tag with the same name' do + let(:id) { 'ambiguous_ref' } + let(:params) { { namespace_id: project.namespace, project_id: project, id: id, ref_type: ref_type } } - it 'redirects to blob#show with sha for the branch' do - expect(response).to redirect_to(project_tree_path(project, RepoHelpers.another_sample_commit.id)) + context 'and explicitly requesting a branch' do + let(:ref_type) { 'heads' } + + it 'redirects to blob#show with sha for the branch' do + request + expect(response).to redirect_to(project_tree_path(project, RepoHelpers.another_sample_commit.id)) + end + end + + context 'and explicitly requesting a tag' do + let(:ref_type) { 'tags' } + + it 'responds with success' do + request + expect(response).to be_ok + end end end + end - context 'and explicitly requesting a tag' do - let(:ref_type) { 'tags' } + describe 'delegating to ExtractsRef::RequestedRef' do + context 'when there is a ref and tag with the same name' do + let(:id) { 'ambiguous_ref' } + let(:params) { { namespace_id: project.namespace, project_id: project, id: id, ref_type: ref_type } } - it 'responds with success' do - expect(response).to be_ok + let(:requested_ref_double) { ExtractsRef::RequestedRef.new(project.repository, ref_type: ref_type, ref: id) } + + before do + allow(ExtractsRef::RequestedRef).to receive(:new).with(kind_of(Repository), ref_type: ref_type, ref: id).and_return(requested_ref_double) + end + + context 'and not specifying a ref_type' do + it 'finds the tags and redirects' do + expect(requested_ref_double).to receive(:find).and_call_original + request + expect(subject).to redirect_to("/#{project.full_path}/-/tree/#{id}/?ref_type=tags") + end + end + + context 'and explicitly requesting a branch' do + let(:ref_type) { 'heads' } + + it 'finds the branch' do + expect(requested_ref_double).not_to receive(:find) + request + expect(response).to be_ok + end + end + + context 'and explicitly requesting a tag' do + let(:ref_type) { 'tags' } + + it 'finds the tag' do + expect(requested_ref_double).not_to receive(:find) + request + expect(response).to be_ok + end end end end @@ -55,19 +112,26 @@ RSpec.describe Projects::TreeController, feature_category: :source_code_manageme context "valid branch, no path" do let(:id) { 'master' } - it { is_expected.to respond_with(:success) } + it 'responds with success' do + request + expect(response).to be_ok + end end context "valid branch, valid path" do let(:id) { 'master/encoding/' } - it { is_expected.to respond_with(:success) } + it 'responds with success' do + request + expect(response).to be_ok + end end context "valid branch, invalid path" do let(:id) { 'master/invalid-path/' } it 'redirects' do + request expect(subject) .to redirect_to("/#{project.full_path}/-/tree/master") end @@ -76,54 +140,91 @@ RSpec.describe Projects::TreeController, feature_category: :source_code_manageme context "invalid branch, valid path" do let(:id) { 'invalid-branch/encoding/' } - it { is_expected.to respond_with(:not_found) } + it 'responds with not_found' do + request + expect(subject).to respond_with(:not_found) + end end - context "renamed default branch, valid file" do - let(:id) { 'old-default-branch/encoding/' } - let(:previous_default_branch) { 'old-default-branch' } + context 'when default branch was renamed' do + let_it_be_with_reload(:project) { create(:project, :repository, previous_default_branch: 'old-default-branch') } - it { is_expected.to redirect_to("/#{project.full_path}/-/tree/#{project.default_branch}/encoding/") } - end + context "and the file is valid" do + let(:id) { 'old-default-branch/encoding/' } + + it 'redirects' do + request + expect(subject).to redirect_to("/#{project.full_path}/-/tree/#{project.default_branch}/encoding/") + end + end - context "renamed default branch, invalid file" do - let(:id) { 'old-default-branch/invalid-path/' } - let(:previous_default_branch) { 'old-default-branch' } + context "and the file is invalid" do + let(:id) { 'old-default-branch/invalid-path/' } - it { is_expected.to redirect_to("/#{project.full_path}/-/tree/#{project.default_branch}/invalid-path/") } + it 'redirects' do + request + expect(subject).to redirect_to("/#{project.full_path}/-/tree/#{project.default_branch}/invalid-path/") + end + end end context "valid empty branch, invalid path" do let(:id) { 'empty-branch/invalid-path/' } it 'redirects' do - expect(subject) - .to redirect_to("/#{project.full_path}/-/tree/empty-branch") + request + expect(subject).to redirect_to("/#{project.full_path}/-/tree/empty-branch") end end context "valid empty branch" do let(:id) { 'empty-branch' } - it { is_expected.to respond_with(:success) } + it 'responds with success' do + request + expect(response).to be_ok + end end context "invalid SHA commit ID" do let(:id) { 'ff39438/.gitignore' } - it { is_expected.to respond_with(:not_found) } + it 'responds with not_found' do + request + expect(subject).to respond_with(:not_found) + end end context "valid SHA commit ID" do let(:id) { '6d39438' } - it { is_expected.to respond_with(:success) } + it 'responds with success' do + request + expect(response).to be_ok + end + + context 'and there is a tag with the same name' do + before do + project.repository.add_tag(project.creator, id, RepoHelpers.sample_commit.id) + end + + it 'responds with success' do + request + + # This uses the tag + # TODO: Should we redirect in this case? + expect(response).to be_ok + end + end end context "valid SHA commit ID with path" do let(:id) { '6d39438/.gitignore' } - it { expect(response).to have_gitlab_http_status(:found) } + it 'responds with found' do + request + expect(response).to have_gitlab_http_status(:found) + end end end @@ -149,7 +250,7 @@ RSpec.describe Projects::TreeController, feature_category: :source_code_manageme before do get :show, params: { - namespace_id: project.namespace.to_param, project_id: project, id: id + namespace_id: project.namespace.to_param, project_id: project, id: id, ref_type: 'heads' } end @@ -157,7 +258,7 @@ RSpec.describe Projects::TreeController, feature_category: :source_code_manageme let(:id) { 'master/README.md' } it 'redirects' do - redirect_url = "/#{project.full_path}/-/blob/master/README.md" + redirect_url = "/#{project.full_path}/-/blob/master/README.md?ref_type=heads" expect(subject).to redirect_to(redirect_url) end end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 6adddccfda7..46913cfa649 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -164,107 +164,113 @@ RSpec.describe ProjectsController, feature_category: :groups_and_projects do end end - context 'when there is a tag with the same name as the default branch' do - let_it_be(:tagged_project) { create(:project, :public, :custom_repo, files: ['somefile']) } - let(:tree_with_default_branch) do - branch = tagged_project.repository.find_branch(tagged_project.default_branch) - project_tree_path(tagged_project, branch.target) - end - + context 'when redirect_with_ref_type is disabled' do before do - tagged_project.repository.create_file( - tagged_project.creator, - 'file_for_tag', - 'content for file', - message: "Automatically created file", - branch_name: 'branch-to-tag' - ) - - tagged_project.repository.add_tag( - tagged_project.creator, - tagged_project.default_branch, # tag name - 'branch-to-tag' # target - ) - end - - it 'redirects to tree view for the default branch' do - get :show, params: { namespace_id: tagged_project.namespace, id: tagged_project } - expect(response).to redirect_to(tree_with_default_branch) - end - end - - context 'when the default branch name is ambiguous' do - let_it_be(:project_with_default_branch) do - create(:project, :public, :custom_repo, files: ['somefile']) + stub_feature_flags(redirect_with_ref_type: false) end - shared_examples 'ambiguous ref redirects' do - let(:project) { project_with_default_branch } - let(:branch_ref) { "refs/heads/#{ref}" } - let(:repo) { project.repository } + context 'when there is a tag with the same name as the default branch' do + let_it_be(:tagged_project) { create(:project, :public, :custom_repo, files: ['somefile']) } + let(:tree_with_default_branch) do + branch = tagged_project.repository.find_branch(tagged_project.default_branch) + project_tree_path(tagged_project, branch.target) + end before do - repo.create_branch(branch_ref, 'master') - repo.change_head(ref) + tagged_project.repository.create_file( + tagged_project.creator, + 'file_for_tag', + 'content for file', + message: "Automatically created file", + branch_name: 'branch-to-tag' + ) + + tagged_project.repository.add_tag( + tagged_project.creator, + tagged_project.default_branch, # tag name + 'branch-to-tag' # target + ) end - after do - repo.change_head('master') - repo.delete_branch(branch_ref) + it 'redirects to tree view for the default branch' do + get :show, params: { namespace_id: tagged_project.namespace, id: tagged_project } + expect(response).to redirect_to(tree_with_default_branch) end + end - subject do - get( - :show, - params: { - namespace_id: project.namespace, - id: project - } - ) + context 'when the default branch name is ambiguous' do + let_it_be(:project_with_default_branch) do + create(:project, :public, :custom_repo, files: ['somefile']) end - context 'when there is no conflicting ref' do - let(:other_ref) { 'non-existent-ref' } + shared_examples 'ambiguous ref redirects' do + let(:project) { project_with_default_branch } + let(:branch_ref) { "refs/heads/#{ref}" } + let(:repo) { project.repository } - it { is_expected.to have_gitlab_http_status(:ok) } - end + before do + repo.create_branch(branch_ref, 'master') + repo.change_head(ref) + end + + after do + repo.change_head('master') + repo.delete_branch(branch_ref) + end - context 'and that other ref exists' do - let(:other_ref) { 'master' } + subject do + get( + :show, + params: { + namespace_id: project.namespace, + id: project + } + ) + end + + context 'when there is no conflicting ref' do + let(:other_ref) { 'non-existent-ref' } - let(:project_default_root_tree_path) do - sha = repo.find_branch(project.default_branch).target - project_tree_path(project, sha) + it { is_expected.to have_gitlab_http_status(:ok) } end - it 'redirects to tree view for the default branch' do - is_expected.to redirect_to(project_default_root_tree_path) + context 'and that other ref exists' do + let(:other_ref) { 'master' } + + let(:project_default_root_tree_path) do + sha = repo.find_branch(project.default_branch).target + project_tree_path(project, sha) + end + + it 'redirects to tree view for the default branch' do + is_expected.to redirect_to(project_default_root_tree_path) + end end end - end - context 'when ref starts with ref/heads/' do - let(:ref) { "refs/heads/#{other_ref}" } + context 'when ref starts with ref/heads/' do + let(:ref) { "refs/heads/#{other_ref}" } - include_examples 'ambiguous ref redirects' - end + include_examples 'ambiguous ref redirects' + end - context 'when ref starts with ref/tags/' do - let(:ref) { "refs/tags/#{other_ref}" } + context 'when ref starts with ref/tags/' do + let(:ref) { "refs/tags/#{other_ref}" } - include_examples 'ambiguous ref redirects' - end + include_examples 'ambiguous ref redirects' + end - context 'when ref starts with heads/' do - let(:ref) { "heads/#{other_ref}" } + context 'when ref starts with heads/' do + let(:ref) { "heads/#{other_ref}" } - include_examples 'ambiguous ref redirects' - end + include_examples 'ambiguous ref redirects' + end - context 'when ref starts with tags/' do - let(:ref) { "tags/#{other_ref}" } + context 'when ref starts with tags/' do + let(:ref) { "tags/#{other_ref}" } - include_examples 'ambiguous ref redirects' + include_examples 'ambiguous ref redirects' + end end end end diff --git a/spec/factories/external_pull_requests.rb b/spec/factories/ci/external_pull_requests.rb index 470814f4360..9a16e400101 100644 --- a/spec/factories/external_pull_requests.rb +++ b/spec/factories/ci/external_pull_requests.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true FactoryBot.define do - factory :external_pull_request do + factory :external_pull_request, class: 'Ci::ExternalPullRequest' do sequence(:pull_request_iid) project source_branch { 'feature' } diff --git a/spec/factories/integrations.rb b/spec/factories/integrations.rb index c3ae0279c05..a89edc19cc7 100644 --- a/spec/factories/integrations.rb +++ b/spec/factories/integrations.rb @@ -136,6 +136,8 @@ FactoryBot.define do jira_auth_type: evaluator.jira_auth_type, jira_issue_transition_automatic: evaluator.jira_issue_transition_automatic, jira_issue_transition_id: evaluator.jira_issue_transition_id, + jira_issue_prefix: evaluator.jira_issue_prefix, + jira_issue_regex: evaluator.jira_issue_regex, username: evaluator.username, password: evaluator.password, issues_enabled: evaluator.issues_enabled, project_key: evaluator.project_key, vulnerabilities_enabled: evaluator.vulnerabilities_enabled, vulnerabilities_issuetype: evaluator.vulnerabilities_issuetype, deployment_type: evaluator.deployment_type diff --git a/spec/features/issues/user_edits_issue_spec.rb b/spec/features/issues/user_edits_issue_spec.rb index bc20660d2a0..7e54580b085 100644 --- a/spec/features/issues/user_edits_issue_spec.rb +++ b/spec/features/issues/user_edits_issue_spec.rb @@ -125,7 +125,7 @@ RSpec.describe "Issues > User edits issue", :js, feature_category: :team_plannin expect(issuable_form).to have_selector(markdown_field_focused_selector) page.within issuable_form do - click_button("Switch to rich text") + click_button("Switch to rich text editing") end expect(issuable_form).not_to have_selector(content_editor_focused_selector) @@ -137,7 +137,7 @@ RSpec.describe "Issues > User edits issue", :js, feature_category: :team_plannin expect(issuable_form).to have_selector(content_editor_focused_selector) page.within issuable_form do - click_button("Switch to Markdown") + click_button("Switch to plain text editing") end expect(issuable_form).not_to have_selector(markdown_field_focused_selector) diff --git a/spec/features/merge_request/user_posts_notes_spec.rb b/spec/features/merge_request/user_posts_notes_spec.rb index a749821b083..d31777db42e 100644 --- a/spec/features/merge_request/user_posts_notes_spec.rb +++ b/spec/features/merge_request/user_posts_notes_spec.rb @@ -131,16 +131,16 @@ RSpec.describe 'Merge request > User posts notes', :js, feature_category: :code_ describe 'when previewing a note' do it 'shows the toolbar buttons when editing a note' do - page.within('.js-main-target-form') do - expect(page).to have_css('.md-header-toolbar') + page.within('.js-main-target-form .md-header-toolbar') do + expect(page).to have_css('button', count: 16) end end it 'hides the toolbar buttons when previewing a note' do wait_for_requests click_button("Preview") - page.within('.js-main-target-form') do - expect(page).not_to have_css('.md-header-toolbar') + page.within('.js-main-target-form .md-header-toolbar') do + expect(page).to have_css('button', count: 1) end end end diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js index 0b8321ba8eb..816c9458201 100644 --- a/spec/frontend/content_editor/components/content_editor_spec.js +++ b/spec/frontend/content_editor/components/content_editor_spec.js @@ -14,6 +14,7 @@ import FormattingToolbar from '~/content_editor/components/formatting_toolbar.vu import LoadingIndicator from '~/content_editor/components/loading_indicator.vue'; import waitForPromises from 'helpers/wait_for_promises'; import { KEYDOWN_EVENT } from '~/content_editor/constants'; +import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue'; jest.mock('~/emoji'); @@ -92,19 +93,6 @@ describe('ContentEditor', () => { expect(wrapper.findComponent(FormattingToolbar).exists()).toBe(true); }); - it('renders footer containing quick actions help text if quick actions docs path is defined', () => { - createWrapper({ quickActionsDocsPath: '/foo/bar' }); - - expect(wrapper.text()).toContain('For quick actions, type /'); - expect(wrapper.findComponent(GlLink).attributes('href')).toBe('/foo/bar'); - }); - - it('does not render footer containing quick actions help text if quick actions docs path is not defined', () => { - createWrapper(); - - expect(findEditorElement().text()).not.toContain('For quick actions, type /'); - }); - it('displays an attachment button', () => { createWrapper(); @@ -286,4 +274,10 @@ describe('ContentEditor', () => { expect(wrapper.findComponent(component).exists()).toBe(true); }); + + it('renders an editor mode dropdown', () => { + createWrapper(); + + expect(wrapper.findComponent(EditorModeSwitcher).exists()).toBe(true); + }); }); diff --git a/spec/frontend/content_editor/components/formatting_toolbar_spec.js b/spec/frontend/content_editor/components/formatting_toolbar_spec.js index 9d835381ff4..0f0198a6425 100644 --- a/spec/frontend/content_editor/components/formatting_toolbar_spec.js +++ b/spec/frontend/content_editor/components/formatting_toolbar_spec.js @@ -6,7 +6,6 @@ import { TOOLBAR_CONTROL_TRACKING_ACTION, CONTENT_EDITOR_TRACKING_LABEL, } from '~/content_editor/constants'; -import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue'; describe('content_editor/components/formatting_toolbar', () => { let wrapper; @@ -17,7 +16,6 @@ describe('content_editor/components/formatting_toolbar', () => { stubs: { GlTabs, GlTab, - EditorModeSwitcher, }, propsData: props, }); @@ -69,12 +67,6 @@ describe('content_editor/components/formatting_toolbar', () => { }); }); - it('renders an editor mode dropdown', () => { - buildWrapper(); - - expect(wrapper.findComponent(EditorModeSwitcher).exists()).toBe(true); - }); - describe('when attachment button is hidden', () => { it('does not show the attachment button', () => { buildWrapper({ hideAttachmentButton: true }); diff --git a/spec/frontend/design_management/components/design_description/description_form_spec.js b/spec/frontend/design_management/components/design_description/description_form_spec.js index 8c01023b1a8..1eb0e8898d4 100644 --- a/spec/frontend/design_management/components/design_description/description_form_spec.js +++ b/spec/frontend/design_management/components/design_description/description_form_spec.js @@ -139,7 +139,6 @@ describe('Design description form', () => { mockDesign.id, )}`, markdownDocsPath: '/help/user/markdown', - quickActionsDocsPath: '/help/user/project/quick_actions', }); }); diff --git a/spec/frontend/invite_members/components/members_token_select_spec.js b/spec/frontend/invite_members/components/members_token_select_spec.js index ff0313cc49e..925534edd7c 100644 --- a/spec/frontend/invite_members/components/members_token_select_spec.js +++ b/spec/frontend/invite_members/components/members_token_select_spec.js @@ -143,12 +143,19 @@ describe('MembersTokenSelect', () => { }); describe('when input text is an email', () => { - it('allows user defined tokens', async () => { - tokenSelector.vm.$emit('text-input', 'foo@bar.com'); + it.each` + email | result + ${'foo@bar.com'} | ${true} + ${'foo@bar.com '} | ${false} + ${' foo@bar.com'} | ${false} + ${'foo@ba r.com'} | ${false} + ${'fo o@bar.com'} | ${false} + `(`with token creation validation on $email`, async ({ email, result }) => { + tokenSelector.vm.$emit('text-input', email); await nextTick(); - expect(tokenSelector.props('allowUserDefinedTokens')).toBe(true); + expect(tokenSelector.props('allowUserDefinedTokens')).toBe(result); }); }); }); diff --git a/spec/frontend/issues/show/components/fields/description_spec.js b/spec/frontend/issues/show/components/fields/description_spec.js index c7116f380a1..36074e07055 100644 --- a/spec/frontend/issues/show/components/fields/description_spec.js +++ b/spec/frontend/issues/show/components/fields/description_spec.js @@ -16,7 +16,6 @@ describe('Description field component', () => { propsData: { markdownPreviewPath: '/', markdownDocsPath: '/', - quickActionsDocsPath: '/', value: description, }, provide: { @@ -80,7 +79,6 @@ describe('Description field component', () => { renderMarkdownPath: '/', autofocus: true, supportsQuickActions: true, - quickActionsDocsPath: expect.any(String), markdownDocsPath: '/', enableAutocomplete: true, }); diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js index 0179d2afa9e..703f17cc77a 100644 --- a/spec/frontend/notes/components/comment_form_spec.js +++ b/spec/frontend/notes/components/comment_form_spec.js @@ -294,13 +294,13 @@ describe('issue_comment_form component', () => { it('hides content editor switcher if feature flag content_editor_on_issues is off', () => { mountComponent({ mountFunction: mount, features: { contentEditorOnIssues: false } }); - expect(wrapper.text()).not.toContain('Switch to rich text'); + expect(wrapper.text()).not.toContain('Switch to rich text editing'); }); it('shows content editor switcher if feature flag content_editor_on_issues is on', () => { mountComponent({ mountFunction: mount, features: { contentEditorOnIssues: true } }); - expect(wrapper.text()).toContain('Switch to rich text'); + expect(wrapper.text()).toContain('Switch to rich text editing'); }); describe('textarea', () => { @@ -346,15 +346,7 @@ describe('issue_comment_form component', () => { const { markdownDocsPath } = notesDataMock; - expect(wrapper.find(`a[href="${markdownDocsPath}"]`).text()).toBe('Markdown'); - }); - - it('should link to quick actions docs', () => { - mountComponent({ mountFunction: mount }); - - const { quickActionsDocsPath } = notesDataMock; - - expect(wrapper.find(`a[href="${quickActionsDocsPath}"]`).text()).toBe('quick actions'); + expect(wrapper.find(`[href="${markdownDocsPath}"]`).exists()).toBe(true); }); it('should resize textarea after note discarded', async () => { diff --git a/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js index b5b33607282..149f7ad3485 100644 --- a/spec/frontend/notes/components/note_form_spec.js +++ b/spec/frontend/notes/components/note_form_spec.js @@ -66,13 +66,13 @@ describe('issue_note_form component', () => { it('hides content editor switcher if feature flag content_editor_on_issues is off', () => { createComponentWrapper({}, { contentEditorOnIssues: false }); - expect(wrapper.text()).not.toContain('Switch to rich text'); + expect(wrapper.text()).not.toContain('Switch to rich text editing'); }); it('shows content editor switcher if feature flag content_editor_on_issues is on', () => { createComponentWrapper({}, { contentEditorOnIssues: true }); - expect(wrapper.text()).toContain('Switch to rich text'); + expect(wrapper.text()).toContain('Switch to rich text editing'); }); describe('conflicts editing', () => { diff --git a/spec/frontend/notes/components/notes_app_spec.js b/spec/frontend/notes/components/notes_app_spec.js index 0f70b264326..0b38f7ffcb7 100644 --- a/spec/frontend/notes/components/notes_app_spec.js +++ b/spec/frontend/notes/components/notes_app_spec.js @@ -250,15 +250,7 @@ describe('note_app', () => { it('should render markdown docs url', () => { const { markdownDocsPath } = mockData.notesDataMock; - expect(wrapper.find(`a[href="${markdownDocsPath}"]`).text().trim()).toEqual('Markdown'); - }); - - it('should render quick action docs url', () => { - const { quickActionsDocsPath } = mockData.notesDataMock; - - expect(wrapper.find(`a[href="${quickActionsDocsPath}"]`).text().trim()).toEqual( - 'quick actions', - ); + expect(wrapper.find(`a[href="${markdownDocsPath}"]`).exists()).toBe(true); }); }); @@ -274,19 +266,7 @@ describe('note_app', () => { const { markdownDocsPath } = mockData.notesDataMock; await nextTick(); - expect(wrapper.find(`.edit-note a[href="${markdownDocsPath}"]`).text().trim()).toEqual( - 'Markdown', - ); - }); - - it('should render quick actions docs url', async () => { - wrapper.find('.js-note-edit').trigger('click'); - const { quickActionsDocsPath } = mockData.notesDataMock; - - await nextTick(); - expect(wrapper.find(`.edit-note a[href="${quickActionsDocsPath}"]`).text().trim()).toEqual( - 'quick actions', - ); + expect(wrapper.find(`.edit-note a[href="${markdownDocsPath}"]`).exists()).toBe(true); }); }); diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap index 9f2b91cb7fd..ec3d028db1a 100644 --- a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap +++ b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap @@ -83,7 +83,6 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] = <markdown-toolbar-stub canattachfile="true" markdowndocspath="help/" - quickactionsdocspath="" showcommenttoolbar="true" /> </div> diff --git a/spec/frontend/vue_shared/components/markdown/editor_mode_switcher_spec.js b/spec/frontend/vue_shared/components/markdown/editor_mode_switcher_spec.js index 693353ed604..f8d5faf317c 100644 --- a/spec/frontend/vue_shared/components/markdown/editor_mode_switcher_spec.js +++ b/spec/frontend/vue_shared/components/markdown/editor_mode_switcher_spec.js @@ -17,8 +17,8 @@ describe('vue_shared/component/markdown/editor_mode_switcher', () => { describe.each` modeText | value | buttonText - ${'Rich text'} | ${'richText'} | ${'Switch to Markdown'} - ${'Markdown'} | ${'markdown'} | ${'Switch to rich text'} + ${'Rich text'} | ${'richText'} | ${'Switch to plain text editing'} + ${'Markdown'} | ${'markdown'} | ${'Switch to rich text editing'} `('when $modeText', ({ modeText, value, buttonText }) => { beforeEach(() => { createComponent({ value }); diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js index b29f0d58d77..37d18455bf2 100644 --- a/spec/frontend/vue_shared/components/markdown/field_spec.js +++ b/spec/frontend/vue_shared/components/markdown/field_spec.js @@ -65,6 +65,7 @@ describe('Markdown field component', () => { enablePreview, restrictedToolBarItems, showContentEditorSwitcher, + supportsQuickActions: true, }, }, ); @@ -206,12 +207,12 @@ describe('Markdown field component', () => { expect(findMarkdownToolbar().props()).toEqual({ canAttachFile: true, markdownDocsPath, - quickActionsDocsPath: '', showCommentToolBar: true, + showContentEditorSwitcher: false, }); expect(findMarkdownHeader().props()).toMatchObject({ - showContentEditorSwitcher: false, + supportsQuickActions: true, }); }); }); @@ -368,13 +369,13 @@ describe('Markdown field component', () => { it('defaults to false', () => { createSubject(); - expect(findMarkdownHeader().props('showContentEditorSwitcher')).toBe(false); + expect(findMarkdownToolbar().props('showContentEditorSwitcher')).toBe(false); }); it('passes showContentEditorSwitcher', () => { createSubject({ showContentEditorSwitcher: true }); - expect(findMarkdownHeader().props('showContentEditorSwitcher')).toBe(true); + expect(findMarkdownToolbar().props('showContentEditorSwitcher')).toBe(true); }); }); }); diff --git a/spec/frontend/vue_shared/components/markdown/header_spec.js b/spec/frontend/vue_shared/components/markdown/header_spec.js index 48fe5452e74..0d973bb9afc 100644 --- a/spec/frontend/vue_shared/components/markdown/header_spec.js +++ b/spec/frontend/vue_shared/components/markdown/header_spec.js @@ -1,11 +1,10 @@ import $ from 'jquery'; import { nextTick } from 'vue'; -import { GlToggle } from '@gitlab/ui'; +import { GlToggle, GlButton } from '@gitlab/ui'; import HeaderComponent from '~/vue_shared/components/markdown/header.vue'; import ToolbarButton from '~/vue_shared/components/markdown/toolbar_button.vue'; import DrawioToolbarButton from '~/vue_shared/components/markdown/drawio_toolbar_button.vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue'; describe('Markdown field header component', () => { let wrapper; @@ -100,7 +99,8 @@ describe('Markdown field header component', () => { it('hides toolbar in preview mode', () => { createWrapper({ previewMarkdown: true }); - expect(findToolbar().classes().includes('gl-display-none!')).toBe(true); + // only one button is rendered in preview mode + expect(findToolbar().findAllComponents(GlButton)).toHaveLength(1); }); it('emits toggle markdown event when clicking preview toggle', async () => { @@ -205,18 +205,4 @@ describe('Markdown field header component', () => { }); }); }); - - describe('with content editor switcher', () => { - beforeEach(() => { - createWrapper({ - showContentEditorSwitcher: true, - }); - }); - - it('re-emits event from switcher', () => { - wrapper.findComponent(EditorModeSwitcher).vm.$emit('input', 'richText'); - - expect(wrapper.emitted('enableContentEditor')).toEqual([[]]); - }); - }); }); diff --git a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js index e54e261b8e4..2119bd1cd27 100644 --- a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js +++ b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js @@ -29,7 +29,6 @@ describe('vue_shared/component/markdown/markdown_editor', () => { const value = 'test markdown'; const renderMarkdownPath = '/api/markdown'; const markdownDocsPath = '/help/markdown'; - const quickActionsDocsPath = '/help/quickactions'; const enableAutocomplete = true; const enablePreview = false; const formFieldId = 'markdown_field'; @@ -43,7 +42,6 @@ describe('vue_shared/component/markdown/markdown_editor', () => { value, renderMarkdownPath, markdownDocsPath, - quickActionsDocsPath, enableAutocomplete, autocompleteDataSources, enablePreview, @@ -110,7 +108,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => { expect(findMarkdownField().props()).toMatchObject({ autocompleteDataSources, markdownPreviewPath: renderMarkdownPath, - quickActionsDocsPath, + supportsQuickActions: true, canAttachFile: true, enableAutocomplete, textareaValue: value, @@ -145,13 +143,13 @@ describe('vue_shared/component/markdown/markdown_editor', () => { it('enables content editor switcher when contentEditorEnabled prop is true', () => { buildWrapper({ propsData: { enableContentEditor: true } }); - expect(findMarkdownField().text()).toContain('Switch to rich text'); + expect(findMarkdownField().text()).toContain('Switch to rich text editing'); }); it('hides content editor switcher when contentEditorEnabled prop is false', () => { buildWrapper({ propsData: { enableContentEditor: false } }); - expect(findMarkdownField().text()).not.toContain('Switch to rich text'); + expect(findMarkdownField().text()).not.toContain('Switch to rich text editing'); }); it('passes down any additional props to markdown field component', () => { diff --git a/spec/frontend/vue_shared/components/markdown/toolbar_spec.js b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js index 2489421b697..28dc6fcde74 100644 --- a/spec/frontend/vue_shared/components/markdown/toolbar_spec.js +++ b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js @@ -1,10 +1,11 @@ import { mount } from '@vue/test-utils'; import Toolbar from '~/vue_shared/components/markdown/toolbar.vue'; +import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue'; describe('toolbar', () => { let wrapper; - const createMountedWrapper = (props = {}) => { + const createWrapper = (props = {}) => { wrapper = mount(Toolbar, { propsData: { markdownDocsPath: '', ...props }, }); @@ -12,7 +13,7 @@ describe('toolbar', () => { describe('user can attach file', () => { beforeEach(() => { - createMountedWrapper(); + createWrapper(); }); it('should render uploading-container', () => { @@ -22,7 +23,7 @@ describe('toolbar', () => { describe('user cannot attach file', () => { beforeEach(() => { - createMountedWrapper({ canAttachFile: false }); + createWrapper({ canAttachFile: false }); }); it('should not render uploading-container', () => { @@ -32,15 +33,29 @@ describe('toolbar', () => { describe('comment tool bar settings', () => { it('does not show comment tool bar div', () => { - createMountedWrapper({ showCommentToolBar: false }); + createWrapper({ showCommentToolBar: false }); expect(wrapper.find('.comment-toolbar').exists()).toBe(false); }); it('shows comment tool bar by default', () => { - createMountedWrapper(); + createWrapper(); expect(wrapper.find('.comment-toolbar').exists()).toBe(true); }); }); + + describe('with content editor switcher', () => { + beforeEach(() => { + createWrapper({ + showContentEditorSwitcher: true, + }); + }); + + it('re-emits event from switcher', () => { + wrapper.findComponent(EditorModeSwitcher).vm.$emit('input', 'richText'); + + expect(wrapper.emitted('enableContentEditor')).toEqual([[]]); + }); + }); }); diff --git a/spec/frontend/work_items/components/work_item_description_spec.js b/spec/frontend/work_items/components/work_item_description_spec.js index 2b3fdd1b31c..8b9963b2476 100644 --- a/spec/frontend/work_items/components/work_item_description_spec.js +++ b/spec/frontend/work_items/components/work_item_description_spec.js @@ -91,7 +91,6 @@ describe('WorkItemDescription', () => { expect(findMarkdownEditor().props()).toMatchObject({ supportsQuickActions: true, renderMarkdownPath: markdownPreviewPath(fullPath, iid), - quickActionsDocsPath: wrapper.vm.$options.quickActionsDocsPath, autocompleteDataSources: autocompleteDataSources(fullPath, iid), }); }); diff --git a/spec/lib/atlassian/jira_connect/client_spec.rb b/spec/lib/atlassian/jira_connect/client_spec.rb index 66ae3658a92..f7597579e7a 100644 --- a/spec/lib/atlassian/jira_connect/client_spec.rb +++ b/spec/lib/atlassian/jira_connect/client_spec.rb @@ -214,13 +214,7 @@ RSpec.describe Atlassian::JiraConnect::Client, feature_category: :integrations d end describe '#store_deploy_info' do - let_it_be(:environment) { create(:environment, name: 'DEV', project: project) } - let_it_be(:deployments) do - pipelines.map do |p| - build = create(:ci_build, environment: environment.name, pipeline: p, project: project) - create(:deployment, deployable: build, environment: environment) - end - end + let_it_be(:deployments) { create_list(:deployment, 1) } let(:schema) do Atlassian::Schemata.deploy_info_payload @@ -252,18 +246,22 @@ RSpec.describe Atlassian::JiraConnect::Client, feature_category: :integrations d subject.send(:store_deploy_info, project: project, deployments: deployments) end - it 'only sends information about relevant MRs' do + it 'calls the API if issue keys are found' do expect(subject).to receive(:post).with( - '/rest/deployments/0.1/bulk', { deployments: have_attributes(size: 8) } + '/rest/deployments/0.1/bulk', { deployments: have_attributes(size: 1) } ).and_call_original subject.send(:store_deploy_info, project: project, deployments: deployments) end - it 'does not call the API if there is nothing to report' do + it 'does not call the API if no issue keys are found' do + allow_next_instances_of(Atlassian::JiraConnect::Serializers::DeploymentEntity, nil) do |entity| + allow(entity).to receive(:issue_keys).and_return([]) + end + expect(subject).not_to receive(:post) - subject.send(:store_deploy_info, project: project, deployments: deployments.take(1)) + subject.send(:store_deploy_info, project: project, deployments: deployments) end context 'when there are errors' do diff --git a/spec/lib/atlassian/jira_connect/serializers/deployment_entity_spec.rb b/spec/lib/atlassian/jira_connect/serializers/deployment_entity_spec.rb index 523b7ddaa09..57e0b67e9e6 100644 --- a/spec/lib/atlassian/jira_connect/serializers/deployment_entity_spec.rb +++ b/spec/lib/atlassian/jira_connect/serializers/deployment_entity_spec.rb @@ -6,18 +6,16 @@ RSpec.describe Atlassian::JiraConnect::Serializers::DeploymentEntity, feature_ca let_it_be(:user) { create_default(:user) } let_it_be(:project) { create_default(:project, :repository) } let_it_be(:environment) { create(:environment, name: 'prod', project: project) } - let_it_be_with_reload(:deployment) { create(:deployment, environment: environment) } + let_it_be_with_refind(:deployment) { create(:deployment, environment: environment) } subject { described_class.represent(deployment) } - context 'when the deployment does not belong to any Jira issue' do - describe '#issue_keys' do - it 'is empty' do - expect(subject.issue_keys).to be_empty + describe '#to_json' do + context 'when the deployment does not belong to any Jira issue' do + before do + allow(subject).to receive(:issue_keys).and_return([]) end - end - describe '#to_json' do it 'can encode the object' do expect(subject.to_json).to be_valid_json end @@ -26,9 +24,19 @@ RSpec.describe Atlassian::JiraConnect::Serializers::DeploymentEntity, feature_ca expect(subject.to_json).not_to match_schema(Atlassian::Schemata.deployment_info) end end + + context 'when the deployment belongs to Jira issue' do + before do + allow(subject).to receive(:issue_keys).and_return(['JIRA-1']) + end + + it 'is valid according to the deployment info schema' do + expect(subject.to_json).to be_valid_json.and match_schema(Atlassian::Schemata.deployment_info) + end + end end - context 'this is an external deployment' do + context 'when deployment is an external deployment' do before do deployment.update!(deployable: nil) end @@ -36,10 +44,6 @@ RSpec.describe Atlassian::JiraConnect::Serializers::DeploymentEntity, feature_ca it 'does not raise errors when serializing' do expect { subject.to_json }.not_to raise_error end - - it 'returns an empty list of issue keys' do - expect(subject.issue_keys).to be_empty - end end describe 'environment type' do @@ -62,27 +66,137 @@ RSpec.describe Atlassian::JiraConnect::Serializers::DeploymentEntity, feature_ca end end - context 'when the deployment can be linked to a Jira issue' do - let(:pipeline) { create(:ci_pipeline, merge_request: merge_request) } - + describe '#issue_keys' do + # For these tests, use a Jira issue key regex that matches a set of commit messages + # in the test repo. + # + # Relevant commits in this test from https://gitlab.com/gitlab-org/gitlab-test/-/commits/master: + # + # 1) 5f923865dde3436854e9ceb9cdb7815618d4e849 GitLab currently doesn't support patches [...]: add a commit here + # 2) 4cd80ccab63c82b4bad16faa5193fbd2aa06df40 add directory structure for tree_helper spec + # 3) ae73cb07c9eeaf35924a10f713b364d32b2dd34f Binary file added + # 4) 33f3729a45c02fc67d00adb1b8bca394b0e761d9 Image added before do - subject.deployable.update!(pipeline: pipeline) + allow(Gitlab::Regex).to receive(:jira_issue_key_regex).and_return(/add.[a-d]/) + end + + let(:expected_issue_keys) { ['add a', 'add d', 'added'] } + + it 'extracts issue keys from the commits' do + expect(subject.issue_keys).to contain_exactly(*expected_issue_keys) + end + + it 'limits the number of commits scanned' do + stub_const("#{described_class}::COMMITS_LIMIT", 10) + + expect(subject.issue_keys).to contain_exactly('add a') + end + + context 'when `jira_deployment_issue_keys` flag is disabled' do + before do + stub_feature_flags(jira_deployment_issue_keys: false) + end + + it 'does not extract issue keys from commits' do + expect(subject.issue_keys).to be_empty + end + end + + context 'when deploy happened at an older commit' do + before do + # SHA is from a commit between 1) and 2) in the commit list above. + deployment.update!(sha: 'c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd') + end + + it 'extracts only issue keys from that commit or older' do + expect(subject.issue_keys).to contain_exactly('add d', 'added') + end end - %i[jira_branch jira_title jira_description].each do |trait| - context "because it belongs to an MR with a #{trait}" do - let(:merge_request) { create(:merge_request, trait) } + context 'when the deployment has an associated merge request' do + let_it_be(:pipeline) do + create(:ci_pipeline, + merge_request: create(:merge_request, + title: 'Title addxa', + description: "Description\naddxa\naddya", + source_branch: 'feature/addza' + ) + ) + end + + before do + subject.deployable.update!(pipeline: pipeline) + end + + it 'includes issue keys extracted from the merge request' do + expect(subject.issue_keys).to contain_exactly( + *(expected_issue_keys + %w[addxa addya addza]) + ) + end + end + + context 'when there was a successful deploy to the environment' do + let_it_be_with_reload(:last_deploy) do + # SHA is from a commit between 2) and 3) in the commit list above. + sha = '5937ac0a7beb003549fc5fd26fc247adbce4a52e' + create(:deployment, :success, sha: sha, environment: environment, finished_at: 1.hour.ago) + end + + shared_examples 'extracts only issue keys from commits made since that deployment' do + specify do + expect(subject.issue_keys).to contain_exactly('add a', 'add d') + end + end + + shared_examples 'ignores that deployment' do + specify do + expect(subject.issue_keys).to contain_exactly(*expected_issue_keys) + end + end + + it_behaves_like 'extracts only issue keys from commits made since that deployment' + + context 'when the deploy was for a different environment' do + before do + last_deploy.update!(environment: create(:environment)) + end + + it_behaves_like 'ignores that deployment' + end + + context 'when the deploy was for a different branch or tag' do + before do + last_deploy.update!(ref: 'foo') + end + + it_behaves_like 'ignores that deployment' + end + + context 'when the deploy was not successful' do + before do + last_deploy.drop! + end + + it_behaves_like 'ignores that deployment' + end + + context 'when the deploy commit cannot be found' do + before do + last_deploy.update!(sha: 'foo') + end + + it_behaves_like 'ignores that deployment' + end - describe '#issue_keys' do - it 'is not empty' do - expect(subject.issue_keys).not_to be_empty - end + context 'when there is a more recent deployment' do + let_it_be(:more_recent_last_deploy) do + # SHA is from a commit between 1) and 2) in the commit list above. + sha = 'c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd' + create(:deployment, :success, sha: sha, environment: environment, finished_at: 1.minute.ago) end - describe '#to_json' do - it 'is valid according to the deployment info schema' do - expect(subject.to_json).to be_valid_json.and match_schema(Atlassian::Schemata.deployment_info) - end + it 'extracts only issue keys from commits made since that deployment' do + expect(subject.issue_keys).to contain_exactly('add a') end end end diff --git a/spec/lib/bitbucket/connection_spec.rb b/spec/lib/bitbucket/connection_spec.rb index 58a05c52b9f..2b35a37558c 100644 --- a/spec/lib/bitbucket/connection_spec.rb +++ b/spec/lib/bitbucket/connection_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Bitbucket::Connection do +RSpec.describe Bitbucket::Connection, feature_category: :integrations do let(:token) { 'token' } before do @@ -13,6 +13,16 @@ RSpec.describe Bitbucket::Connection do describe '#get' do it 'calls OAuth2::AccessToken::get' do + expected_client_options = { + site: OmniAuth::Strategies::Bitbucket.default_options[:client_options]['site'], + authorize_url: OmniAuth::Strategies::Bitbucket.default_options[:client_options]['authorize_url'], + token_url: OmniAuth::Strategies::Bitbucket.default_options[:client_options]['token_url'] + } + + expect(OAuth2::Client) + .to receive(:new) + .with(anything, anything, expected_client_options) + expect_next_instance_of(OAuth2::AccessToken) do |instance| expect(instance).to receive(:get).and_return(double(parsed: true)) end diff --git a/spec/lib/extracts_ref/requested_ref_spec.rb b/spec/lib/extracts_ref/requested_ref_spec.rb new file mode 100644 index 00000000000..80d3575b360 --- /dev/null +++ b/spec/lib/extracts_ref/requested_ref_spec.rb @@ -0,0 +1,153 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ExtractsRef::RequestedRef, feature_category: :source_code_management do + describe '#find' do + subject { described_class.new(project.repository, ref_type: ref_type, ref: ref).find } + + let_it_be(:project) { create(:project, :repository) } + let(:ref_type) { nil } + + # Create branches and tags consistently with the same shas to make comparison easier to follow + let(:tag_sha) { RepoHelpers.sample_commit.id } + let(:branch_sha) { RepoHelpers.another_sample_commit.id } + + shared_context 'when a branch exists' do + before do + project.repository.create_branch(branch_name, branch_sha) + end + + after do + project.repository.rm_branch(project.owner, branch_name) + end + end + + shared_context 'when a tag exists' do + before do + project.repository.add_tag(project.owner, tag_name, tag_sha) + end + + after do + project.repository.rm_tag(project.owner, tag_name) + end + end + + shared_examples 'RequestedRef when ref_type is specified' do |branch_sha, tag_sha| + context 'when ref_type is heads' do + let(:ref_type) { 'heads' } + + it 'returns the branch commit' do + expect(subject[:ref_type]).to eq('heads') + expect(subject[:commit].id).to eq(branch_sha) + end + end + + context 'when ref_type is tags' do + let(:ref_type) { 'tags' } + + it 'returns the tag commit' do + expect(subject[:ref_type]).to eq('tags') + expect(subject[:commit].id).to eq(tag_sha) + end + end + end + + context 'when the ref is the sha for a commit' do + let(:ref) { branch_sha } + + context 'and a tag and branch with that sha as a name' do + include_context 'when a branch exists' do + let(:branch_name) { ref } + end + + include_context 'when a tag exists' do + let(:tag_name) { ref } + end + + it_behaves_like 'RequestedRef when ref_type is specified', + RepoHelpers.another_sample_commit.id, + RepoHelpers.sample_commit.id + + it 'returns the commit' do + expect(subject[:ref_type]).to be_nil + expect(subject[:commit].id).to eq(ref) + end + end + end + + context 'when ref is for a tag' do + include_context 'when a tag exists' do + let(:tag_name) { SecureRandom.uuid } + end + + let(:ref) { tag_name } + + it 'returns the tag commit' do + expect(subject[:ref_type]).to eq('tags') + expect(subject[:commit].id).to eq(tag_sha) + end + + context 'and there is a branch with the same name' do + include_context 'when a branch exists' do + let(:branch_name) { ref } + end + + it_behaves_like 'RequestedRef when ref_type is specified', + RepoHelpers.another_sample_commit.id, + RepoHelpers.sample_commit.id + + it 'returns the tag commit' do + expect(subject[:ref_type]).to eq('tags') + expect(subject[:commit].id).to eq(tag_sha) + expect(subject[:ambiguous]).to be_truthy + end + end + end + + context 'when ref is only for a branch' do + let(:ref) { SecureRandom.uuid } + + include_context 'when a branch exists' do + let(:branch_name) { ref } + end + + it 'returns the branch commit' do + expect(subject[:ref_type]).to eq('heads') + expect(subject[:commit].id).to eq(branch_sha) + end + end + + context 'when ref is an abbreviated commit sha' do + let(:ref) { branch_sha.first(8) } + + it 'returns the commit' do + expect(subject[:ref_type]).to be_nil + expect(subject[:commit].id).to eq(branch_sha) + end + end + + context 'when ref does not exist' do + let(:ref) { SecureRandom.uuid } + + it 'returns the commit' do + expect(subject[:ref_type]).to be_nil + expect(subject[:commit]).to be_nil + end + end + + context 'when ref is symbolic' do + let(:ref) { "heads/#{branch_name}" } + + include_context 'when a branch exists' do + let(:branch_name) { SecureRandom.uuid } + end + + it 'returns the commit' do + expect(subject[:ref_type]).to be_nil + expect(subject[:commit].id).to eq(branch_sha) + expect(subject[:ambiguous]).to be_truthy + end + end + end +end diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index abdd8741377..e1825342ebd 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -905,7 +905,7 @@ List: - max_issue_count - max_issue_weight - limit_metric -ExternalPullRequest: +Ci::ExternalPullRequest: - id - created_at - updated_at diff --git a/spec/lib/gitlab/legacy_github_import/client_spec.rb b/spec/lib/gitlab/legacy_github_import/client_spec.rb index d0f63d11469..757bdd8dd6c 100644 --- a/spec/lib/gitlab/legacy_github_import/client_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/client_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::LegacyGithubImport::Client do +RSpec.describe Gitlab::LegacyGithubImport::Client, feature_category: :importers do let(:token) { '123456' } let(:github_provider) { GitlabSettings::Options.build('app_id' => 'asd123', 'app_secret' => 'asd123', 'name' => 'github', 'args' => { 'client_options' => {} }) } let(:wait_for_rate_limit_reset) { true } @@ -47,7 +47,7 @@ RSpec.describe Gitlab::LegacyGithubImport::Client do end describe '#api_endpoint' do - context 'when provider does not specity an API endpoint' do + context 'when provider does not specify an API endpoint' do it 'uses GitHub root API endpoint' do expect(client.api.api_endpoint).to eq 'https://api.github.com/' end diff --git a/spec/models/external_pull_request_spec.rb b/spec/models/ci/external_pull_request_spec.rb index 10136dd0bdb..2a273146626 100644 --- a/spec/models/external_pull_request_spec.rb +++ b/spec/models/ci/external_pull_request_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe ExternalPullRequest do +RSpec.describe Ci::ExternalPullRequest, feature_category: :continuous_integration do let_it_be(:project) { create(:project, :repository) } let(:source_branch) { 'the-branch' } @@ -228,12 +228,12 @@ RSpec.describe ExternalPullRequest do it 'returns modified paths' do expect(modified_paths).to eq ['bar/branch-test.txt', - 'files/js/commit.coffee', - 'with space/README.md'] + 'files/js/commit.coffee', + 'with space/README.md'] end end - context 'loose foreign key on external_pull_requests.project_id' do + context 'with a loose foreign key on external_pull_requests.project_id' do it_behaves_like 'cleanup by a loose foreign key' do let!(:parent) { create(:project) } let!(:model) { create(:external_pull_request, project: parent) } diff --git a/spec/services/members/invite_service_spec.rb b/spec/services/members/invite_service_spec.rb index 1c0466980f4..76cd5d6c89e 100644 --- a/spec/services/members/invite_service_spec.rb +++ b/spec/services/members/invite_service_spec.rb @@ -219,6 +219,18 @@ RSpec.describe Members::InviteService, :aggregate_failures, :clean_gitlab_redis_ expect(result[:message][params[:email]]).to eq("Invite email is invalid") end end + + context 'with email that has trailing spaces' do + let(:params) { { email: ' foo@bar.com ' } } + + it 'returns an error' do + expect_not_to_create_members + expect(result[:status]).to eq(:error) + expect(result[:message][params[:email]]).to eq("Invite email is invalid") + end + + it_behaves_like 'does not record an onboarding progress action' + end end context 'with duplicate invites' do diff --git a/spec/support/helpers/content_editor_helpers.rb b/spec/support/helpers/content_editor_helpers.rb index 83c18f8073f..a3ecafbbec6 100644 --- a/spec/support/helpers/content_editor_helpers.rb +++ b/spec/support/helpers/content_editor_helpers.rb @@ -2,7 +2,7 @@ module ContentEditorHelpers def switch_to_content_editor - click_button("Switch to rich text") + click_button("Switch to rich text editing") end def type_in_content_editor(keys) diff --git a/spec/support/helpers/next_instance_of.rb b/spec/support/helpers/next_instance_of.rb index 3c88715615d..5cc63fe5c6e 100644 --- a/spec/support/helpers/next_instance_of.rb +++ b/spec/support/helpers/next_instance_of.rb @@ -31,8 +31,9 @@ module NextInstanceOf receive_new.exactly(number).times end - target.to receive_new.and_wrap_original do |method, *original_args| - method.call(*original_args).tap(&blk) + target.to receive_new.and_wrap_original do |*original_args, **original_kwargs| + method, *original_args = original_args + method.call(*original_args, **original_kwargs).tap(&blk) end end end diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml index 3f8e504a296..0c4ae894de7 100644 --- a/spec/support/rspec_order_todo.yml +++ b/spec/support/rspec_order_todo.yml @@ -5248,7 +5248,6 @@ - './spec/lib/atlassian/jira_connect/serializers/base_entity_spec.rb' - './spec/lib/atlassian/jira_connect/serializers/branch_entity_spec.rb' - './spec/lib/atlassian/jira_connect/serializers/build_entity_spec.rb' -- './spec/lib/atlassian/jira_connect/serializers/deployment_entity_spec.rb' - './spec/lib/atlassian/jira_connect/serializers/feature_flag_entity_spec.rb' - './spec/lib/atlassian/jira_connect/serializers/pull_request_entity_spec.rb' - './spec/lib/atlassian/jira_connect/serializers/repository_entity_spec.rb' diff --git a/spec/support/shared_examples/features/inviting_members_shared_examples.rb b/spec/support/shared_examples/features/inviting_members_shared_examples.rb index 2eca2a72997..178f85cb85b 100644 --- a/spec/support/shared_examples/features/inviting_members_shared_examples.rb +++ b/spec/support/shared_examples/features/inviting_members_shared_examples.rb @@ -159,6 +159,40 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label| end end + context 'when a user already exists, and private email is used' do + it 'fails with an error', :js do + visit subentity_members_page_path + + invite_member(user2.email, role: role) + + invite_modal = page.find(invite_modal_selector) + expect(invite_modal).to have_content "#{user2.email}: Access level should be greater than or equal to " \ + "Developer inherited membership from group #{group.name}" + + page.refresh + + page.within find_invited_member_row(user2.name) do + expect(page).to have_content('Developer') + expect(page).not_to have_button('Developer') + end + end + + it 'does not allow inviting of an email that has spaces', :js do + visit subentity_members_page_path + + click_on _('Invite members') + + page.within invite_modal_selector do + choose_options(role, nil) + find(member_dropdown_selector).set("#{user2.email} ") + wait_for_requests + + expect(page).to have_content('No matches found') + expect(page).not_to have_button("#{user2.email} ") + end + end + end + context 'when there are multiple users invited with errors' do let_it_be(:user3) { create(:user) } diff --git a/spec/views/shared/notes/_form.html.haml_spec.rb b/spec/views/shared/notes/_form.html.haml_spec.rb deleted file mode 100644 index ccf1e08b7e7..00000000000 --- a/spec/views/shared/notes/_form.html.haml_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'shared/notes/_form' do - include Devise::Test::ControllerHelpers - - let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - - before do - project.add_maintainer(user) - assign(:project, project) - assign(:note, note) - - allow(view).to receive(:current_user).and_return(user) - - render - end - - %w[issue merge_request commit].each do |noteable| - context "with a note on #{noteable}" do - let(:note) { build(:"note_on_#{noteable}", project: project) } - - it 'says that markdown and quick actions are supported' do - expect(rendered).to have_content('Supports Markdown. For quick actions, type /.') - end - end - end -end diff --git a/vendor/gems/sidekiq-reliable-fetch/.gitlab-ci.yml b/vendor/gems/sidekiq-reliable-fetch/.gitlab-ci.yml index b87a454bccc..fdf9ccdeb55 100644 --- a/vendor/gems/sidekiq-reliable-fetch/.gitlab-ci.yml +++ b/vendor/gems/sidekiq-reliable-fetch/.gitlab-ci.yml @@ -50,12 +50,6 @@ integration_reliable: variables: JOB_FETCHER: reliable -integration_basic: - extends: .integration - allow_failure: yes - variables: - JOB_FETCHER: basic - kill_interruption: stage: test script: |