diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-10-09 00:09:48 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-10-09 00:09:48 +0300 |
commit | 59e6c2df22c69baa791529db3326e68c9de10b54 (patch) | |
tree | aa75309a037a6031c38f8ccd9afe53cbcd519355 | |
parent | e7527f548681e4f9efd32f9c3da937ad74c68948 (diff) |
Add latest changes from gitlab-org/gitlab@master
59 files changed, 851 insertions, 230 deletions
diff --git a/.gitlab/merge_request_templates/Deprecations.md b/.gitlab/merge_request_templates/Deprecations.md index ec2b1879407..48e6e35e961 100644 --- a/.gitlab/merge_request_templates/Deprecations.md +++ b/.gitlab/merge_request_templates/Deprecations.md @@ -75,4 +75,4 @@ yourself as a reviewer if it's not ready for merge yet. </details> When the PM indicates it is ready for merge, all issues have been addressed, and -the doc has been properly regenerated with the Rake task, merge the MR. +the doc has been properly regenerated with the [Rake task](https://about.gitlab.com/handbook/marketing/blog/release-posts/#update-the-deprecations-doc), merge the MR. diff --git a/app/assets/javascripts/content_editor/components/top_toolbar.vue b/app/assets/javascripts/content_editor/components/top_toolbar.vue index 82a449ae6af..89182b3a09f 100644 --- a/app/assets/javascripts/content_editor/components/top_toolbar.vue +++ b/app/assets/javascripts/content_editor/components/top_toolbar.vue @@ -112,6 +112,15 @@ export default { @execute="trackToolbarControlExecution" /> <toolbar-button + data-testid="details" + content-type="details" + icon-name="details-block" + class="gl-mx-2" + editor-command="toggleDetails" + :label="__('Add a collapsible section')" + @execute="trackToolbarControlExecution" + /> + <toolbar-button data-testid="horizontal-rule" content-type="horizontalRule" icon-name="dash" diff --git a/app/assets/javascripts/content_editor/components/wrappers/details.vue b/app/assets/javascripts/content_editor/components/wrappers/details.vue new file mode 100644 index 00000000000..aff15ac3e53 --- /dev/null +++ b/app/assets/javascripts/content_editor/components/wrappers/details.vue @@ -0,0 +1,33 @@ +<script> +import { NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2'; + +export default { + name: 'DetailsWrapper', + components: { + NodeViewWrapper, + NodeViewContent, + }, + props: { + node: { + type: Object, + required: true, + }, + }, + data() { + return { + open: true, + }; + }, +}; +</script> +<template> + <node-view-wrapper class="gl-display-flex"> + <div + class="details-toggle-icon" + data-testid="details-toggle-icon" + :class="{ 'is-open': open }" + @click="open = !open" + ></div> + <node-view-content as="ul" class="details-content" :class="{ 'is-open': open }" /> + </node-view-wrapper> +</template> diff --git a/app/assets/javascripts/content_editor/extensions/details.js b/app/assets/javascripts/content_editor/extensions/details.js new file mode 100644 index 00000000000..e3d54ed01fd --- /dev/null +++ b/app/assets/javascripts/content_editor/extensions/details.js @@ -0,0 +1,36 @@ +import { Node } from '@tiptap/core'; +import { VueNodeViewRenderer } from '@tiptap/vue-2'; +import { wrappingInputRule } from 'prosemirror-inputrules'; +import DetailsWrapper from '../components/wrappers/details.vue'; + +export const inputRegex = /^\s*(<details>)$/; + +export default Node.create({ + name: 'details', + content: 'detailsContent+', + // eslint-disable-next-line @gitlab/require-i18n-strings + group: 'block list', + + parseHTML() { + return [{ tag: 'details' }]; + }, + + renderHTML({ HTMLAttributes }) { + return ['ul', HTMLAttributes, 0]; + }, + + addNodeView() { + return VueNodeViewRenderer(DetailsWrapper); + }, + + addInputRules() { + return [wrappingInputRule(inputRegex, this.type)]; + }, + + addCommands() { + return { + setDetails: () => ({ commands }) => commands.wrapInList('details'), + toggleDetails: () => ({ commands }) => commands.toggleList('details', 'detailsContent'), + }; + }, +}); diff --git a/app/assets/javascripts/content_editor/extensions/details_content.js b/app/assets/javascripts/content_editor/extensions/details_content.js new file mode 100644 index 00000000000..fb6c49d91aa --- /dev/null +++ b/app/assets/javascripts/content_editor/extensions/details_content.js @@ -0,0 +1,25 @@ +import { Node } from '@tiptap/core'; +import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants'; + +export default Node.create({ + name: 'detailsContent', + content: 'block+', + defining: true, + + parseHTML() { + return [ + { tag: '*', consuming: false, context: 'details/', priority: PARSE_HTML_PRIORITY_HIGHEST }, + ]; + }, + + renderHTML({ HTMLAttributes }) { + return ['li', HTMLAttributes, 0]; + }, + + addKeyboardShortcuts() { + return { + Enter: () => this.editor.commands.splitListItem('detailsContent'), + 'Shift-Tab': () => this.editor.commands.liftListItem('detailsContent'), + }; + }, +}); diff --git a/app/assets/javascripts/content_editor/services/create_content_editor.js b/app/assets/javascripts/content_editor/services/create_content_editor.js index 0471adf67e9..3b4fda26a36 100644 --- a/app/assets/javascripts/content_editor/services/create_content_editor.js +++ b/app/assets/javascripts/content_editor/services/create_content_editor.js @@ -10,6 +10,8 @@ import Code from '../extensions/code'; import CodeBlockHighlight from '../extensions/code_block_highlight'; import DescriptionItem from '../extensions/description_item'; import DescriptionList from '../extensions/description_list'; +import Details from '../extensions/details'; +import DetailsContent from '../extensions/details_content'; import Division from '../extensions/division'; import Document from '../extensions/document'; import Dropcursor from '../extensions/dropcursor'; @@ -81,6 +83,8 @@ export const createContentEditor = ({ CodeBlockHighlight, DescriptionItem, DescriptionList, + Details, + DetailsContent, Document, Division, Dropcursor, diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js index 19b67f6fde8..8b6cff3f2e1 100644 --- a/app/assets/javascripts/content_editor/services/markdown_serializer.js +++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js @@ -11,6 +11,8 @@ import Code from '../extensions/code'; import CodeBlockHighlight from '../extensions/code_block_highlight'; import DescriptionItem from '../extensions/description_item'; import DescriptionList from '../extensions/description_list'; +import Details from '../extensions/details'; +import DetailsContent from '../extensions/details_content'; import Division from '../extensions/division'; import Emoji from '../extensions/emoji'; import Figure from '../extensions/figure'; @@ -53,6 +55,7 @@ import { renderImage, renderPlayable, renderHTMLNode, + renderContent, } from './serialization_helpers'; const defaultSerializerConfig = { @@ -133,6 +136,15 @@ const defaultSerializerConfig = { renderHTMLNode(node.attrs.isTerm ? 'dt' : 'dd')(state, node); if (index === parent.childCount - 1) state.ensureNewLine(); }, + [Details.name]: renderHTMLNode('details', true), + [DetailsContent.name]: (state, node, parent, index) => { + if (!index) renderHTMLNode('summary')(state, node); + else { + if (index === 1) state.ensureNewLine(); + renderContent(state, node); + if (index === parent.childCount - 1) state.ensureNewLine(); + } + }, [Emoji.name]: (state, node) => { const { name } = node.attrs; diff --git a/app/assets/javascripts/design_management/pages/design/index.vue b/app/assets/javascripts/design_management/pages/design/index.vue index 38ea5406c02..837320b9423 100644 --- a/app/assets/javascripts/design_management/pages/design/index.vue +++ b/app/assets/javascripts/design_management/pages/design/index.vue @@ -273,7 +273,7 @@ export default { this.onError(UPDATE_IMAGE_DIFF_NOTE_ERROR, e); }, onDesignDeleteError(e) { - this.onError(designDeletionError({ singular: true }), e); + this.onError(designDeletionError(), e); }, onResolveDiscussionError(e) { this.onError(UPDATE_IMAGE_DIFF_NOTE_ERROR, e); diff --git a/app/assets/javascripts/design_management/pages/index.vue b/app/assets/javascripts/design_management/pages/index.vue index e66ae822a34..5092c30aa60 100644 --- a/app/assets/javascripts/design_management/pages/index.vue +++ b/app/assets/javascripts/design_management/pages/index.vue @@ -255,7 +255,7 @@ export default { if (this.$route.query.version) this.$router.push({ name: DESIGNS_ROUTE_NAME }); }, onDesignDeleteError() { - const errorMessage = designDeletionError({ singular: this.selectedDesigns.length === 1 }); + const errorMessage = designDeletionError(this.selectedDesigns.length); createFlash({ message: errorMessage }); }, onDesignDropzoneError() { diff --git a/app/assets/javascripts/design_management/utils/cache_update.js b/app/assets/javascripts/design_management/utils/cache_update.js index 33c4fd5a7d9..c8f445bfb88 100644 --- a/app/assets/javascripts/design_management/utils/cache_update.js +++ b/app/assets/javascripts/design_management/utils/cache_update.js @@ -250,7 +250,7 @@ export const hasErrors = ({ errors = [] }) => errors?.length; */ export const updateStoreAfterDesignsDelete = (store, data, query, designs) => { if (hasErrors(data)) { - onError(data, designDeletionError({ singular: designs.length === 1 })); + onError(data, designDeletionError(designs.length)); } else { deleteDesignsFromStore(store, query, designs); addNewVersionToStore(store, query, data.version); diff --git a/app/assets/javascripts/design_management/utils/error_messages.js b/app/assets/javascripts/design_management/utils/error_messages.js index afee7e81791..981b50329b2 100644 --- a/app/assets/javascripts/design_management/utils/error_messages.js +++ b/app/assets/javascripts/design_management/utils/error_messages.js @@ -1,4 +1,3 @@ -/* eslint-disable @gitlab/require-string-literal-i18n-helpers */ import { __, s__, n__, sprintf } from '~/locale'; export const ADD_DISCUSSION_COMMENT_ERROR = s__( @@ -27,12 +26,6 @@ export const DESIGN_NOT_FOUND_ERROR = __('Could not find design.'); export const DESIGN_VERSION_NOT_EXIST_ERROR = __('Requested design version does not exist.'); -const DESIGN_UPLOAD_SKIPPED_MESSAGE = s__('DesignManagement|Upload skipped.'); - -const ALL_DESIGNS_SKIPPED_MESSAGE = `${DESIGN_UPLOAD_SKIPPED_MESSAGE} ${s__( - 'The designs you tried uploading did not change.', -)}`; - export const EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE = __( 'You can only upload one design when dropping onto an existing design.', ); @@ -53,12 +46,9 @@ export const DELETE_DESIGN_TODO_ERROR = __('Failed to remove a to-do item for th export const TOGGLE_TODO_ERROR = __('Failed to toggle the to-do status for the design.'); -const MAX_SKIPPED_FILES_LISTINGS = 5; +const DESIGN_UPLOAD_SKIPPED_MESSAGE = s__('DesignManagement|Upload skipped. %{reason}'); -const oneDesignSkippedMessage = (filename) => - `${DESIGN_UPLOAD_SKIPPED_MESSAGE} ${sprintf(s__('DesignManagement|%{filename} did not change.'), { - filename, - })}`; +const MAX_SKIPPED_FILES_LISTINGS = 5; /** * Return warning message indicating that some (but not all) uploaded @@ -66,25 +56,40 @@ const oneDesignSkippedMessage = (filename) => * @param {Array<{ filename }>} skippedFiles */ const someDesignsSkippedMessage = (skippedFiles) => { - const designsSkippedMessage = `${DESIGN_UPLOAD_SKIPPED_MESSAGE} ${s__( - 'Some of the designs you tried uploading did not change:', - )}`; - - const moreText = sprintf(s__(`DesignManagement|and %{moreCount} more.`), { - moreCount: skippedFiles.length - MAX_SKIPPED_FILES_LISTINGS, - }); - - return `${designsSkippedMessage} ${skippedFiles + const skippedFilesList = skippedFiles .slice(0, MAX_SKIPPED_FILES_LISTINGS) .map(({ filename }) => filename) - .join(', ')}${skippedFiles.length > MAX_SKIPPED_FILES_LISTINGS ? `, ${moreText}` : '.'}`; + .join(', '); + + const uploadSkippedReason = + skippedFiles.length > MAX_SKIPPED_FILES_LISTINGS + ? sprintf( + s__( + 'DesignManagement|Some of the designs you tried uploading did not change: %{skippedFiles} and %{moreCount} more.', + ), + { + skippedFiles: skippedFilesList, + moreCount: skippedFiles.length - MAX_SKIPPED_FILES_LISTINGS, + }, + ) + : sprintf( + s__( + 'DesignManagement|Some of the designs you tried uploading did not change: %{skippedFiles}.', + ), + { skippedFiles: skippedFilesList }, + ); + + return sprintf(DESIGN_UPLOAD_SKIPPED_MESSAGE, { + reason: uploadSkippedReason, + }); }; -export const designDeletionError = ({ singular = true } = {}) => { - const design = singular ? __('a design') : __('designs'); - return sprintf(s__('Could not archive %{design}. Please try again.'), { - design, - }); +export const designDeletionError = (designsCount = 1) => { + return n__( + 'Failed to archive a design. Please try again.', + 'Failed to archive designs. Please try again.', + designsCount, + ); }; /** @@ -101,7 +106,18 @@ export const designUploadSkippedWarning = (uploadedDesigns, skippedFiles) => { if (skippedFiles.length === uploadedDesigns.length) { const { filename } = skippedFiles[0]; - return n__(oneDesignSkippedMessage(filename), ALL_DESIGNS_SKIPPED_MESSAGE, skippedFiles.length); + const uploadSkippedReason = sprintf( + n__( + 'DesignManagement|%{filename} did not change.', + 'DesignManagement|The designs you tried uploading did not change.', + skippedFiles.length, + ), + { filename }, + ); + + return sprintf(DESIGN_UPLOAD_SKIPPED_MESSAGE, { + reason: uploadSkippedReason, + }); } return someDesignsSkippedMessage(skippedFiles); diff --git a/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js b/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js index 8d2d5d41f6a..ee48543f0d2 100644 --- a/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js +++ b/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js @@ -20,7 +20,7 @@ export default class OAuthRememberMe { toggleRememberMe(event) { const rememberMe = $(event.target).is(':checked'); - $('.oauth-login', this.container).each((i, element) => { + $('.js-oauth-login', this.container).each((i, element) => { const $form = $(element).parent('form'); const href = $form.attr('action'); diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue index 3470c963ade..b778fe28e59 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue @@ -5,7 +5,6 @@ import { GlDropdownItem, GlDropdownSectionHeader, GlLoadingIcon, - GlSprintf, GlTooltipDirective, } from '@gitlab/ui'; import axios from '~/lib/utils/axios_utils'; @@ -13,7 +12,6 @@ import { __, s__ } from '~/locale'; export const i18n = { artifacts: __('Artifacts'), - downloadArtifact: __('Download %{name} artifact'), artifactSectionHeader: __('Download artifacts'), artifactsFetchErrorMessage: s__('Pipelines|Could not load artifacts.'), emptyArtifactsMessage: __('No artifacts found'), @@ -30,7 +28,6 @@ export default { GlDropdownItem, GlDropdownSectionHeader, GlLoadingIcon, - GlSprintf, }, inject: { artifactsEndpoint: { @@ -113,9 +110,7 @@ export default { class="gl-word-break-word" data-testid="artifact-item" > - <gl-sprintf :message="$options.i18n.downloadArtifact"> - <template #name>{{ artifact.name }}</template> - </gl-sprintf> + {{ artifact.name }} </gl-dropdown-item> </gl-dropdown> </template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue index 36629d9f1f1..1c7c4d7c704 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue @@ -3,8 +3,8 @@ import { GlAlert, GlDropdown, GlDropdownItem, + GlDropdownSectionHeader, GlLoadingIcon, - GlSprintf, GlTooltipDirective, } from '@gitlab/ui'; import axios from '~/lib/utils/axios_utils'; @@ -12,7 +12,6 @@ import { __, s__ } from '~/locale'; export const i18n = { artifacts: __('Artifacts'), - downloadArtifact: __('Download %{name} artifact'), artifactSectionHeader: __('Download artifacts'), artifactsFetchErrorMessage: s__('Pipelines|Could not load artifacts.'), noArtifacts: s__('Pipelines|No artifacts available'), @@ -27,8 +26,8 @@ export default { GlAlert, GlDropdown, GlDropdownItem, + GlDropdownSectionHeader, GlLoadingIcon, - GlSprintf, }, inject: { artifactsEndpoint: { @@ -92,6 +91,10 @@ export default { text-sr-only @show.once="fetchArtifacts" > + <gl-dropdown-section-header>{{ + $options.i18n.artifactSectionHeader + }}</gl-dropdown-section-header> + <gl-alert v-if="hasError" variant="danger" :dismissible="false"> {{ $options.i18n.artifactsFetchErrorMessage }} </gl-alert> @@ -108,10 +111,9 @@ export default { :href="artifact.path" rel="nofollow" download + class="gl-word-break-word" > - <gl-sprintf :message="$options.i18n.downloadArtifact"> - <template #name>{{ artifact.name }}</template> - </gl-sprintf> + {{ artifact.name }} </gl-dropdown-item> </gl-dropdown> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue index eaa1d6f4cdd..38ee81be555 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue @@ -49,7 +49,7 @@ const MERGE_SUCCESS_STATUS = 'success'; const MERGE_HOOK_VALIDATION_ERROR_STATUS = 'hook_validation_error'; const { transitions } = STATE_MACHINE; -const { MERGE, MERGED, MERGE_FAILURE } = transitions; +const { MERGE, MERGED, MERGE_FAILURE, AUTO_MERGE } = transitions; export default { name: 'ReadyToMerge', @@ -365,7 +365,11 @@ export default { } this.isMakingRequest = true; - this.mr.transitionStateMachine({ transition: MERGE }); + + if (!useAutoMerge) { + this.mr.transitionStateMachine({ transition: MERGE }); + } + this.service .merge(options) .then((res) => res.data) @@ -376,6 +380,7 @@ export default { if (AUTO_MERGE_STRATEGIES.includes(data.status)) { eventHub.$emit('MRWidgetUpdateRequested'); + this.mr.transitionStateMachine({ transition: AUTO_MERGE }); } else if (data.status === MERGE_SUCCESS_STATUS) { this.initiateMergePolling(); } else if (hasError) { diff --git a/app/assets/javascripts/vue_merge_request_widget/constants.js b/app/assets/javascripts/vue_merge_request_widget/constants.js index 297e0cfa363..b88e83ccb0f 100644 --- a/app/assets/javascripts/vue_merge_request_widget/constants.js +++ b/app/assets/javascripts/vue_merge_request_widget/constants.js @@ -58,9 +58,11 @@ const STATE_MACHINE = { states: { IDLE: 'IDLE', MERGING: 'MERGING', + AUTO_MERGE: 'AUTO_MERGE', }, transitions: { MERGE: 'start-merge', + AUTO_MERGE: 'start-auto-merge', MERGE_FAILURE: 'merge-failed', MERGED: 'merge-done', }, @@ -73,6 +75,7 @@ STATE_MACHINE.definition = { [states.IDLE]: { on: { [transitions.MERGE]: states.MERGING, + [transitions.AUTO_MERGE]: states.AUTO_MERGE, }, }, [states.MERGING]: { @@ -81,15 +84,23 @@ STATE_MACHINE.definition = { [transitions.MERGE_FAILURE]: states.IDLE, }, }, + [states.AUTO_MERGE]: { + on: { + [transitions.MERGED]: states.IDLE, + [transitions.MERGE_FAILURE]: states.IDLE, + }, + }, }, }; export const stateToTransitionMap = { [stateKey.merging]: transitions.MERGE, [stateKey.merged]: transitions.MERGED, + [stateKey.autoMergeEnabled]: transitions.AUTO_MERGE, }; export const stateToComponentMap = { [states.MERGING]: classStateMap[stateKey.merging], + [states.AUTO_MERGE]: classStateMap[stateKey.autoMergeEnabled], }; export const EXTENSION_ICONS = { diff --git a/app/assets/stylesheets/components/content_editor.scss b/app/assets/stylesheets/components/content_editor.scss index a013d971efb..673e935ed9d 100644 --- a/app/assets/stylesheets/components/content_editor.scss +++ b/app/assets/stylesheets/components/content_editor.scss @@ -3,7 +3,8 @@ th, li, dd, - dt { + dt, + summary { :first-child { margin-bottom: 0 !important; } @@ -37,6 +38,7 @@ } } + .dl-content { width: 100%; @@ -50,6 +52,38 @@ } } } + + .details-toggle-icon { + cursor: pointer; + z-index: 1; + + &::before { + content: '▶'; + display: inline-block; + width: $gl-spacing-scale-4; + } + + &.is-open::before { + content: '▼'; + } + } + + .details-content { + width: calc(100% - #{$gl-spacing-scale-4}); + + > li { + list-style-type: none; + margin-left: $gl-spacing-scale-2; + } + + > :not(:first-child) { + display: none; + } + + &.is-open > :not(:first-child) { + display: inherit; + } + } } .table-creator-grid-item { diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index c366bf80093..2a46e50f0da 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -196,14 +196,6 @@ label { } } -@include media-breakpoint-down(xs) { - .remember-me { - .remember-me-checkbox { - margin-top: 0; - } - } -} - .input-icon-wrapper, .select-wrapper { position: relative; diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index c40433651ab..cb36c4e5767 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -75,6 +75,15 @@ details { margin-bottom: $gl-padding; + + > *:not(summary) { + margin-left: $gl-spacing-scale-5; + } + } + + summary > * { + display: inline-block; + margin-bottom: 0; } // Single code lines should wrap @@ -478,6 +487,7 @@ font-size: larger; } + figcaption, .small { font-size: smaller; } diff --git a/app/assets/stylesheets/page_bundles/signup.scss b/app/assets/stylesheets/page_bundles/signup.scss index 57e5d2411d1..4fc671dace8 100644 --- a/app/assets/stylesheets/page_bundles/signup.scss +++ b/app/assets/stylesheets/page_bundles/signup.scss @@ -26,14 +26,6 @@ } } - .omniauth-btn { - width: 48%; - - @include media-breakpoint-down(md) { - width: 100%; - } - } - .decline-page { width: 350px; } diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index 45cbef2d1c2..71ddbf175e9 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -99,11 +99,6 @@ padding: 0; border: 0; background: none; - margin-bottom: $gl-padding; - } - - .omniauth-btn { - width: 100%; } } diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss index 2f68694a2a5..d436c328921 100644 --- a/app/assets/stylesheets/startup/startup-dark.scss +++ b/app/assets/stylesheets/startup/startup-dark.scss @@ -5,13 +5,13 @@ body.gl-dark { --gray-50: #303030; --gray-100: #404040; + --gray-900: #fafafa; --gray-950: #fff; --green-100: #0d532a; --green-400: #108548; --green-700: #91d4a8; --blue-400: #1f75cb; --orange-400: #ab6100; - --purple-100: #2f2a6b; --gl-text-color: #fafafa; --border-color: #4f4f4f; --black: #fff; @@ -1785,8 +1785,8 @@ body.gl-dark .nav-sidebar li.active > a { body.gl-dark .nav-sidebar .fly-out-top-item a, body.gl-dark .nav-sidebar .fly-out-top-item.active a, body.gl-dark .nav-sidebar .fly-out-top-item .fly-out-top-item-container { - background-color: var(--purple-100, #e1d8f9); - color: var(--black, #333); + background-color: var(--gray-100, #303030); + color: var(--gray-900, #fafafa); } body.gl-dark .logo-text svg { fill: var(--gl-text-color); diff --git a/app/assets/stylesheets/startup/startup-signin.scss b/app/assets/stylesheets/startup/startup-signin.scss index 013ad3fac87..8d7531d6c9c 100644 --- a/app/assets/stylesheets/startup/startup-signin.scss +++ b/app/assets/stylesheets/startup/startup-signin.scss @@ -258,21 +258,6 @@ fieldset:disabled a.btn { align-items: center; justify-content: space-between; } -.d-block { - display: block !important; -} -.d-flex { - display: flex !important; -} -.flex-wrap { - flex-wrap: wrap !important; -} -.justify-content-between { - justify-content: space-between !important; -} -.align-items-center { - align-items: center !important; -} .fixed-top { position: fixed; top: 0; @@ -280,9 +265,6 @@ fieldset:disabled a.btn { left: 0; z-index: 1030; } -.ml-2 { - margin-left: 0.5rem !important; -} .mt-3 { margin-top: 1rem !important; } @@ -349,6 +331,15 @@ fieldset:disabled a.btn { font-size: 0.875rem; border-radius: 0.25rem; } +.gl-button.gl-button .gl-button-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding-top: 1px; + padding-bottom: 1px; + margin-top: -1px; + margin-bottom: -1px; +} .gl-button.gl-button .gl-button-icon { height: 1rem; width: 1rem; @@ -637,10 +628,6 @@ svg { padding: 0; border: 0; background: none; - margin-bottom: 16px; -} -.login-page .omniauth-container .omniauth-btn { - width: 100%; } .login-page .new-session-tabs { display: flex; @@ -771,21 +758,18 @@ svg { .gl-align-items-center { align-items: center; } +.gl-flex-wrap { + flex-wrap: wrap; +} .gl-w-full { width: 100%; } -.gl-p-2 { - padding: 0.25rem; -} .gl-p-4 { padding: 0.75rem; } .gl-mt-2 { margin-top: 0.25rem; } -.gl-mb-2 { - margin-bottom: 0.25rem; -} .gl-mb-3 { margin-bottom: 0.5rem; } @@ -797,8 +781,8 @@ svg { margin-top: 0; } } -.gl-text-left { - text-align: left; +.gl-font-weight-bold { + font-weight: 600; } @import "startup/cloaking"; diff --git a/app/assets/stylesheets/themes/theme_helper.scss b/app/assets/stylesheets/themes/theme_helper.scss index a9e8b238d78..1332686a906 100644 --- a/app/assets/stylesheets/themes/theme_helper.scss +++ b/app/assets/stylesheets/themes/theme_helper.scss @@ -212,8 +212,8 @@ a:hover, &.active a, .fly-out-top-item-container { - background-color: var(--purple-100, $purple-900); - color: var(--black, $white); + background-color: var(--gray-100, $gray-50); + color: var(--gray-900, $gray-900); } } } diff --git a/app/helpers/startupjs_helper.rb b/app/helpers/startupjs_helper.rb index b595590c7c9..2e8f0cb7dbe 100644 --- a/app/helpers/startupjs_helper.rb +++ b/app/helpers/startupjs_helper.rb @@ -5,6 +5,13 @@ module StartupjsHelper @graphql_startup_calls end + def page_startup_graphql_headers + { + 'X-CSRF-Token' => form_authenticity_token, + 'x-gitlab-feature-category' => ::Gitlab::ApplicationContext.current_context_attribute(:feature_category).presence || '' + } + end + def add_page_startup_graphql_call(query, variables = {}) @graphql_startup_calls ||= [] file_location = File.join(Rails.root, "app/graphql/queries/#{query}.query.graphql") diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml index 82c0df354d4..9015474a868 100644 --- a/app/views/devise/sessions/_new_base.html.haml +++ b/app/views/devise/sessions/_new_base.html.haml @@ -6,10 +6,10 @@ = f.label :password, class: 'label-bold' = f.password_field :password, class: 'form-control gl-form-input bottom', required: true, title: _('This field is required.'), data: { qa_selector: 'password_field' } - if devise_mapping.rememberable? - .remember-me + %div %label{ for: 'user_remember_me' } - = f.check_box :remember_me, class: 'remember-me-checkbox' - %span Remember me + = f.check_box :remember_me + %span= _('Remember me') .float-right - if unconfirmed_email? = link_to _('Resend confirmation email'), new_user_confirmation_path diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml index 1752a43b032..bd7fe41ae8d 100644 --- a/app/views/devise/shared/_omniauth_box.html.haml +++ b/app/views/devise/shared/_omniauth_box.html.haml @@ -1,20 +1,20 @@ - hide_remember_me = local_assigns.fetch(:hide_remember_me, false) .omniauth-container.gl-mt-5 - %label.label-bold.d-block + %label.gl-font-weight-bold = _('Sign in with') - providers = enabled_button_based_providers - .d-flex.justify-content-between.flex-wrap + .gl-display-flex.gl-justify-content-between.gl-flex-wrap - providers.each do |provider| - has_icon = provider_has_icon?(provider) - = button_to omniauth_authorize_path(:user, provider), id: "oauth-login-#{provider}", class: "btn gl-button btn-default omniauth-btn oauth-login #{qa_class_for_provider(provider)}", form: { class: 'gl-w-full' } do + = button_to omniauth_authorize_path(:user, provider), id: "oauth-login-#{provider}", class: "btn gl-button btn-default gl-w-full js-oauth-login #{qa_class_for_provider(provider)}", form: { class: 'gl-w-full gl-mb-3' } do - if has_icon = provider_image_tag(provider) %span.gl-button-text = label_for_provider(provider) - unless hide_remember_me - %fieldset.remember-me + %fieldset %label - = check_box_tag :remember_me, nil, false, class: 'remember-me-checkbox' + = check_box_tag :remember_me, nil, false %span = _('Remember me') diff --git a/app/views/devise/shared/_signup_omniauth_provider_list.haml b/app/views/devise/shared/_signup_omniauth_provider_list.haml index 43e0802ee2a..c24e8770f05 100644 --- a/app/views/devise/shared/_signup_omniauth_provider_list.haml +++ b/app/views/devise/shared/_signup_omniauth_provider_list.haml @@ -1,9 +1,9 @@ -%label.label-bold.d-block +%label.gl-font-weight-bold = _("Create an account using:") -.d-flex.justify-content-between.flex-wrap +.gl-display-flex.gl-justify-content-between.gl-flex-wrap - providers.each do |provider| - = link_to omniauth_authorize_path(:user, provider), method: :post, class: "btn gl-button btn-default gl-display-flex gl-align-items-center gl-text-left gl-mb-2 gl-p-2 omniauth-btn oauth-login #{qa_class_for_provider(provider)}", id: "oauth-login-#{provider}" do + = link_to omniauth_authorize_path(:user, provider), method: :post, class: "btn gl-button btn-default gl-w-full gl-mb-3 js-oauth-login #{qa_class_for_provider(provider)}", id: "oauth-login-#{provider}" do - if provider_has_icon?(provider) = provider_image_tag(provider) - %span.ml-2 + %span.gl-button-text = label_for_provider(provider) diff --git a/app/views/devise/shared/_signup_omniauth_providers.haml b/app/views/devise/shared/_signup_omniauth_providers.haml index a653d44d694..30a54ab86a6 100644 --- a/app/views/devise/shared/_signup_omniauth_providers.haml +++ b/app/views/devise/shared/_signup_omniauth_providers.haml @@ -1,3 +1,3 @@ -.omniauth-divider.d-flex.align-items-center.text-center +.omniauth-divider.gl-display-flex.gl-align-items-center = _("or") = render 'devise/shared/signup_omniauth_provider_list', providers: enabled_button_based_providers diff --git a/app/views/devise/shared/_signup_omniauth_providers_top.haml b/app/views/devise/shared/_signup_omniauth_providers_top.haml index 9a2629443ed..8eb22c0b023 100644 --- a/app/views/devise/shared/_signup_omniauth_providers_top.haml +++ b/app/views/devise/shared/_signup_omniauth_providers_top.haml @@ -1,3 +1,3 @@ = render 'devise/shared/signup_omniauth_provider_list', providers: popular_enabled_button_based_providers -.omniauth-divider.d-flex.align-items-center.text-center +.omniauth-divider.gl-display-flex.gl-align-items-center = _("or") diff --git a/app/views/layouts/_startup_js.html.haml b/app/views/layouts/_startup_js.html.haml index 35cd191c600..b7dd3a9556c 100644 --- a/app/views/layouts/_startup_js.html.haml +++ b/app/views/layouts/_startup_js.html.haml @@ -17,11 +17,14 @@ }); } if (gl.startup_graphql_calls && window.fetch) { + const headers = #{page_startup_graphql_headers.to_json}; const url = `#{api_graphql_url}` const opts = { method: "POST", - headers: { "Content-Type": "application/json", 'X-CSRF-Token': "#{form_authenticity_token}" }, + headers: { + "Content-Type": "application/json", + ...headers, }; gl.startup_graphql_calls = gl.startup_graphql_calls.map(call => ({ diff --git a/doc/development/documentation/styleguide/index.md b/doc/development/documentation/styleguide/index.md index df63cec546d..72491ab3a33 100644 --- a/doc/development/documentation/styleguide/index.md +++ b/doc/development/documentation/styleguide/index.md @@ -1691,6 +1691,16 @@ The development, release, and timing of any products, features, or functionality sole discretion of GitLab Inc. ``` +It renders on the GitLab documentation site as: + +DISCLAIMER: +This page contains information related to upcoming products, features, and functionality. +It is important to note that the information presented is for informational purposes only. +Please do not rely on this information for purchasing or planning purposes. +As with all projects, the items mentioned on this page are subject to change or delay. +The development, release, and timing of any products, features, or functionality remain at the +sole discretion of GitLab Inc. + If all of the content on the page is not available, use the disclaimer once at the top of the page. If the content in a topic is not ready, use the disclaimer in the topic. diff --git a/doc/user/group/import/index.md b/doc/user/group/import/index.md index bcff31c65d1..1f5de36303e 100644 --- a/doc/user/group/import/index.md +++ b/doc/user/group/import/index.md @@ -114,6 +114,8 @@ This might involve reconfiguring your firewall to prevent blocking connection on ![Fill in import details](img/import_panel_v14_1.png) 1. Enter the source URL of your GitLab instance. +1. Generate or copy a [personal access token](../../../user/profile/personal_access_tokens.md) + with the `api` and `read_repository` scopes on your remote GitLab instance. 1. Enter the [personal access token](../../../user/profile/personal_access_tokens.md) for your remote GitLab instance. 1. Select **Connect instance**. diff --git a/doc/user/project/repository/branches/default.md b/doc/user/project/repository/branches/default.md index 3fd96a0c341..5cd025f017d 100644 --- a/doc/user/project/repository/branches/default.md +++ b/doc/user/project/repository/branches/default.md @@ -196,3 +196,32 @@ To fix the problem: ``` 1. In GitLab, [change the default branch](#change-the-default-branch-name-for-a-project) to the one you intend to use. + +### Query GraphQL for default branches + +You can use a [GraphQL query](../../../../api/graphql/index.md) +to retrieve the default branches for all projects in a group. + +To return all projects in a single page of results, replace `GROUPNAME` with the +full path to your group. GitLab returns the first page of results. If `hasNextPage` +is `true`, you can request the next page by replacing the `null` in `after: null` +with the value of `endCursor`: + +```graphql +{ + group(fullPath: "GROUPNAME") { + projects(after: null) { + pageInfo { + hasNextPage + endCursor + } + nodes { + name + repository { + rootRef + } + } + } + } +} +``` diff --git a/doc/user/usage_quotas.md b/doc/user/usage_quotas.md index 07a5eda8cfb..5a48353c9d4 100644 --- a/doc/user/usage_quotas.md +++ b/doc/user/usage_quotas.md @@ -5,7 +5,7 @@ group: Utilization info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers --- -# Storage usage quota **(FREE SAAS)** +# Storage usage quota **(FREE)** > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/13294) in GitLab 12.0. > - Moved to GitLab Free. @@ -18,29 +18,27 @@ you must purchase additional storage. For more details, see [Excess storage usag ## View storage usage -To help manage storage, a namespace's owner can view: +You can view storage usage for your project or [namespace](../user/group/#namespaces). -- Total storage used in the namespace -- Total storage used per project +1. Go to your project or namespace: + - For a project, on the top bar, select **Menu > Projects** and find your project. + - For a namespace, enter the URL in your browser's toolbar. +1. From the left sidebar, select **Settings > Usage Quotas**. +1. Select the **Storage** tab. -To view storage usage, from the namespace's page go to **Settings > Usage Quotas** and select the -**Storage** tab. The Usage Quotas statistics are updated every 90 minutes. +The statistics are displayed. Select any title to view details. The information on this page +is updated every 90 minutes. -If your namespace shows `N/A` as the total storage usage, push a commit to any project in that -namespace to trigger a recalculation. - -A stacked bar graph shows the proportional storage used for the namespace, including a total per -storage item. Click on each project's title to see a breakdown per storage item. +If your namespace shows `N/A`, push a commit to any project in the +namespace to recalculate the storage. ## Storage usage statistics -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/247831) in GitLab 13.7. -> - It's [deployed behind a feature flag](../user/feature_flags.md), enabled by default. -> - It's enabled on GitLab SaaS. -> - It's recommended for production use. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68898) project-level graph in GitLab 14.4 [with a flag](../administration/feature_flags.md) named `project_storage_ui`. Disabled by default. +> - Enabled on GitLab.com in GitLab 14.4. -WARNING: -This feature might not be available to you. Check the **version history** note above for details. +FLAG: +On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../administration/feature_flags.md) named `project_storage_ui`. On GitLab.com, this feature is available. The following storage usage statistics are available to an owner: diff --git a/lib/gitlab/database/count.rb b/lib/gitlab/database/count.rb index eac61254bdf..ce61c1ba9ad 100644 --- a/lib/gitlab/database/count.rb +++ b/lib/gitlab/database/count.rb @@ -35,7 +35,17 @@ module Gitlab # # @param [Array] # @return [Hash] of Model -> count mapping - def self.approximate_counts(models, strategies: [TablesampleCountStrategy, ReltuplesCountStrategy, ExactCountStrategy]) + def self.approximate_counts(models, strategies: []) + if strategies.empty? + # ExactCountStrategy is the only strategy working on read-only DBs, as others make + # use of tuple stats which use the primary DB to estimate tables size in a transaction. + strategies = if ::Gitlab::Database.read_write? + [TablesampleCountStrategy, ReltuplesCountStrategy, ExactCountStrategy] + else + [ExactCountStrategy] + end + end + strategies.each_with_object({}) do |strategy, counts_by_model| models_with_missing_counts = models - counts_by_model.keys diff --git a/lib/tasks/rubocop.rake b/lib/tasks/rubocop.rake index a4147ae1bba..8c5edb5de8a 100644 --- a/lib/tasks/rubocop.rake +++ b/lib/tasks/rubocop.rake @@ -8,13 +8,14 @@ unless Rails.env.production? namespace :rubocop do namespace :todo do desc 'Generate RuboCop todos' - task :generate do + task :generate do # rubocop:disable Rails/RakeEnvironment require 'rubocop' options = %w[ --auto-gen-config --auto-gen-only-exclude --exclude-limit=100000 + --no-offense-counts ] RuboCop::CLI.new.run(options) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 8e73a4ae1ee..7e38030b870 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -9414,9 +9414,6 @@ msgstr "" msgid "Could not apply %{name} command." msgstr "" -msgid "Could not archive %{design}. Please try again." -msgstr "" - msgid "Could not authorize chat nickname. Try again!" msgstr "" @@ -11520,7 +11517,9 @@ msgid "DesignManagement|%{current_design} of %{designs_count}" msgstr "" msgid "DesignManagement|%{filename} did not change." -msgstr "" +msgid_plural "DesignManagement|The designs you tried uploading did not change." +msgstr[0] "" +msgstr[1] "" msgid "DesignManagement|Adding a design with the same filename replaces the file in a new version." msgstr "" @@ -11624,6 +11623,12 @@ msgstr "" msgid "DesignManagement|Select all" msgstr "" +msgid "DesignManagement|Some of the designs you tried uploading did not change: %{skippedFiles} and %{moreCount} more." +msgstr "" + +msgid "DesignManagement|Some of the designs you tried uploading did not change: %{skippedFiles}." +msgstr "" + msgid "DesignManagement|The maximum number of designs allowed to be uploaded is %{upload_limit}. Please try again." msgstr "" @@ -11639,15 +11644,12 @@ msgstr "" msgid "DesignManagement|Upload designs" msgstr "" -msgid "DesignManagement|Upload skipped." +msgid "DesignManagement|Upload skipped. %{reason}" msgstr "" msgid "DesignManagement|Your designs are being copied and are on their way… Please refresh to update." msgstr "" -msgid "DesignManagement|and %{moreCount} more." -msgstr "" - msgid "Designs" msgstr "" @@ -12130,9 +12132,6 @@ msgstr "" msgid "Download %{format}:" msgstr "" -msgid "Download %{name} artifact" -msgstr "" - msgid "Download (%{fileSizeReadable})" msgstr "" @@ -13977,6 +13976,11 @@ msgstr "" msgid "Failed to apply commands." msgstr "" +msgid "Failed to archive a design. Please try again." +msgid_plural "Failed to archive designs. Please try again." +msgstr[0] "" +msgstr[1] "" + msgid "Failed to assign a reviewer because no user was found." msgstr "" @@ -31736,9 +31740,6 @@ msgstr "" msgid "Some common domains are not allowed. %{learn_more_link}." msgstr "" -msgid "Some of the designs you tried uploading did not change:" -msgstr "" - msgid "Someone edited the issue at the same time you did. Please check out %{linkStart}the issue%{linkEnd} and make sure your changes will not unintentionally remove theirs." msgstr "" @@ -33855,9 +33856,6 @@ msgstr "" msgid "The deployment of this job to %{environmentLink} did not succeed." msgstr "" -msgid "The designs you tried uploading did not change." -msgstr "" - msgid "The directory has been successfully created." msgstr "" @@ -39633,9 +39631,6 @@ msgstr "" msgid "a deleted user" msgstr "" -msgid "a design" -msgstr "" - msgid "about 1 hour" msgid_plural "about %d hours" msgstr[0] "" @@ -40173,9 +40168,6 @@ msgstr "" msgid "design" msgstr "" -msgid "designs" -msgstr "" - msgid "detached" msgstr "" diff --git a/spec/frontend/content_editor/components/top_toolbar_spec.js b/spec/frontend/content_editor/components/top_toolbar_spec.js index a5df3d73289..ec58877470c 100644 --- a/spec/frontend/content_editor/components/top_toolbar_spec.js +++ b/spec/frontend/content_editor/components/top_toolbar_spec.js @@ -31,6 +31,7 @@ describe('content_editor/components/top_toolbar', () => { ${'blockquote'} | ${{ contentType: 'blockquote', iconName: 'quote', label: 'Insert a quote', editorCommand: 'toggleBlockquote' }} ${'bullet-list'} | ${{ contentType: 'bulletList', iconName: 'list-bulleted', label: 'Add a bullet list', editorCommand: 'toggleBulletList' }} ${'ordered-list'} | ${{ contentType: 'orderedList', iconName: 'list-numbered', label: 'Add a numbered list', editorCommand: 'toggleOrderedList' }} + ${'details'} | ${{ contentType: 'details', iconName: 'details-block', label: 'Add a collapsible section', editorCommand: 'toggleDetails' }} ${'horizontal-rule'} | ${{ contentType: 'horizontalRule', iconName: 'dash', label: 'Add a horizontal rule', editorCommand: 'setHorizontalRule' }} ${'code-block'} | ${{ contentType: 'codeBlock', iconName: 'doc-code', label: 'Insert a code block', editorCommand: 'toggleCodeBlock' }} ${'text-styles'} | ${{}} diff --git a/spec/frontend/content_editor/components/wrappers/details_spec.js b/spec/frontend/content_editor/components/wrappers/details_spec.js new file mode 100644 index 00000000000..d746b9fa2f1 --- /dev/null +++ b/spec/frontend/content_editor/components/wrappers/details_spec.js @@ -0,0 +1,40 @@ +import { NodeViewContent } from '@tiptap/vue-2'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import DetailsWrapper from '~/content_editor/components/wrappers/details.vue'; + +describe('content/components/wrappers/details', () => { + let wrapper; + + const createWrapper = async () => { + wrapper = shallowMountExtended(DetailsWrapper, { + propsData: { + node: {}, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders a node-view-content as a ul element', () => { + createWrapper(); + + expect(wrapper.findComponent(NodeViewContent).props().as).toBe('ul'); + }); + + it('is "open" by default', () => { + createWrapper(); + + expect(wrapper.findByTestId('details-toggle-icon').classes()).toContain('is-open'); + expect(wrapper.findComponent(NodeViewContent).classes()).toContain('is-open'); + }); + + it('closes the details block on clicking the details toggle icon', async () => { + createWrapper(); + + await wrapper.findByTestId('details-toggle-icon').trigger('click'); + expect(wrapper.findByTestId('details-toggle-icon').classes()).not.toContain('is-open'); + expect(wrapper.findComponent(NodeViewContent).classes()).not.toContain('is-open'); + }); +}); diff --git a/spec/frontend/content_editor/extensions/details_content_spec.js b/spec/frontend/content_editor/extensions/details_content_spec.js new file mode 100644 index 00000000000..575f3bf65e4 --- /dev/null +++ b/spec/frontend/content_editor/extensions/details_content_spec.js @@ -0,0 +1,76 @@ +import Details from '~/content_editor/extensions/details'; +import DetailsContent from '~/content_editor/extensions/details_content'; +import { createTestEditor, createDocBuilder } from '../test_utils'; + +describe('content_editor/extensions/details_content', () => { + let tiptapEditor; + let doc; + let p; + let details; + let detailsContent; + + beforeEach(() => { + tiptapEditor = createTestEditor({ extensions: [Details, DetailsContent] }); + + ({ + builders: { doc, p, details, detailsContent }, + } = createDocBuilder({ + tiptapEditor, + names: { + details: { nodeType: Details.name }, + detailsContent: { nodeType: DetailsContent.name }, + }, + })); + }); + + describe('shortcut: Enter', () => { + it('splits a details content into two items', () => { + const initialDoc = doc( + details( + detailsContent(p('Summary')), + detailsContent(p('Text content')), + detailsContent(p('Text content')), + ), + ); + const expectedDoc = doc( + details( + detailsContent(p('Summary')), + detailsContent(p('')), + detailsContent(p('Text content')), + detailsContent(p('Text content')), + ), + ); + + tiptapEditor.commands.setContent(initialDoc.toJSON()); + + tiptapEditor.commands.setTextSelection(10); + tiptapEditor.commands.keyboardShortcut('Enter'); + + expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON()); + }); + }); + + describe('shortcut: Shift-Tab', () => { + it('lifts a details content and creates two separate details items', () => { + const initialDoc = doc( + details( + detailsContent(p('Summary')), + detailsContent(p('Text content')), + detailsContent(p('Text content')), + ), + ); + const expectedDoc = doc( + details(detailsContent(p('Summary'))), + p('Text content'), + details(detailsContent(p('Text content'))), + ); + + tiptapEditor.commands.setContent(initialDoc.toJSON()); + + tiptapEditor.commands.setTextSelection(20); + tiptapEditor.commands.keyboardShortcut('Shift-Tab'); + + expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON()); + }); + }); +}); diff --git a/spec/frontend/content_editor/extensions/details_spec.js b/spec/frontend/content_editor/extensions/details_spec.js new file mode 100644 index 00000000000..cd59943982f --- /dev/null +++ b/spec/frontend/content_editor/extensions/details_spec.js @@ -0,0 +1,92 @@ +import Details from '~/content_editor/extensions/details'; +import DetailsContent from '~/content_editor/extensions/details_content'; +import { createTestEditor, createDocBuilder } from '../test_utils'; + +describe('content_editor/extensions/details', () => { + let tiptapEditor; + let doc; + let p; + let details; + let detailsContent; + + beforeEach(() => { + tiptapEditor = createTestEditor({ extensions: [Details, DetailsContent] }); + + ({ + builders: { doc, p, details, detailsContent }, + } = createDocBuilder({ + tiptapEditor, + names: { + details: { nodeType: Details.name }, + detailsContent: { nodeType: DetailsContent.name }, + }, + })); + }); + + describe('setDetails command', () => { + describe('when current block is a paragraph', () => { + it('converts current paragraph into a details block', () => { + const initialDoc = doc(p('Text content')); + const expectedDoc = doc(details(detailsContent(p('Text content')))); + + tiptapEditor.commands.setContent(initialDoc.toJSON()); + tiptapEditor.commands.setDetails(); + + expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON()); + }); + }); + + describe('when current block is a details block', () => { + it('maintains the same document structure', () => { + const initialDoc = doc(details(detailsContent(p('Text content')))); + + tiptapEditor.commands.setContent(initialDoc.toJSON()); + tiptapEditor.commands.setDetails(); + + expect(tiptapEditor.getJSON()).toEqual(initialDoc.toJSON()); + }); + }); + }); + + describe('toggleDetails command', () => { + describe('when current block is a paragraph', () => { + it('converts current paragraph into a details block', () => { + const initialDoc = doc(p('Text content')); + const expectedDoc = doc(details(detailsContent(p('Text content')))); + + tiptapEditor.commands.setContent(initialDoc.toJSON()); + tiptapEditor.commands.toggleDetails(); + + expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON()); + }); + }); + + describe('when current block is a details block', () => { + it('convert details block into a paragraph', () => { + const initialDoc = doc(details(detailsContent(p('Text content')))); + const expectedDoc = doc(p('Text content')); + + tiptapEditor.commands.setContent(initialDoc.toJSON()); + tiptapEditor.commands.toggleDetails(); + + expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON()); + }); + }); + }); + + it.each` + input | insertedNode + ${'<details>'} | ${(...args) => details(detailsContent(p(...args)))} + ${'<details'} | ${(...args) => p(...args)} + ${'details>'} | ${(...args) => p(...args)} + `('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => { + const { view } = tiptapEditor; + const { selection } = view.state; + const expectedDoc = doc(insertedNode()); + + // Triggers the event handler that input rules listen to + view.someProp('handleTextInput', (f) => f(view, selection.from, selection.to, input)); + + expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON()); + }); +}); diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js index 6f2c908c289..33056ab9e4a 100644 --- a/spec/frontend/content_editor/services/markdown_serializer_spec.js +++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js @@ -5,6 +5,8 @@ import Code from '~/content_editor/extensions/code'; import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight'; import DescriptionItem from '~/content_editor/extensions/description_item'; import DescriptionList from '~/content_editor/extensions/description_list'; +import Details from '~/content_editor/extensions/details'; +import DetailsContent from '~/content_editor/extensions/details_content'; import Division from '~/content_editor/extensions/division'; import Emoji from '~/content_editor/extensions/emoji'; import Figure from '~/content_editor/extensions/figure'; @@ -45,6 +47,8 @@ const tiptapEditor = createTestEditor({ CodeBlockHighlight, DescriptionItem, DescriptionList, + Details, + DetailsContent, Division, Emoji, Figure, @@ -78,6 +82,8 @@ const { bulletList, code, codeBlock, + details, + detailsContent, division, descriptionItem, descriptionList, @@ -110,6 +116,8 @@ const { bulletList: { nodeType: BulletList.name }, code: { markType: Code.name }, codeBlock: { nodeType: CodeBlockHighlight.name }, + details: { nodeType: Details.name }, + detailsContent: { nodeType: DetailsContent.name }, division: { nodeType: Division.name }, descriptionItem: { nodeType: DescriptionItem.name }, descriptionList: { nodeType: DescriptionList.name }, @@ -588,6 +596,105 @@ A giant _owl-like_ creature. ); }); + it('correctly renders a simple details/summary', () => { + expect( + serialize( + details( + detailsContent(paragraph('this is the summary')), + detailsContent(paragraph('this content will be hidden')), + ), + ), + ).toBe( + ` +<details> +<summary>this is the summary</summary> +this content will be hidden +</details> + `.trim(), + ); + }); + + it('correctly renders details/summary with styled content', () => { + expect( + serialize( + details( + detailsContent(paragraph('this is the ', bold('summary'))), + detailsContent( + codeBlock( + { language: 'javascript' }, + 'var a = 2;\nvar b = 3;\nvar c = a + d;\n\nconsole.log(c);', + ), + ), + detailsContent(paragraph('this content will be ', italic('hidden'))), + ), + details(detailsContent(paragraph('summary 2')), detailsContent(paragraph('content 2'))), + ), + ).toBe( + ` +<details> +<summary> + +this is the **summary** + +</summary> + +\`\`\`javascript +var a = 2; +var b = 3; +var c = a + d; + +console.log(c); +\`\`\` + +this content will be _hidden_ + +</details> +<details> +<summary>summary 2</summary> +content 2 +</details> + `.trim(), + ); + }); + + it('correctly renders nested details', () => { + expect( + serialize( + details( + detailsContent(paragraph('dream level 1')), + detailsContent( + details( + detailsContent(paragraph('dream level 2')), + detailsContent( + details( + detailsContent(paragraph('dream level 3')), + detailsContent(paragraph(italic('inception'))), + ), + ), + ), + ), + ), + ), + ).toBe( + ` +<details> +<summary>dream level 1</summary> + +<details> +<summary>dream level 2</summary> + +<details> +<summary>dream level 3</summary> + +_inception_ + +</details> +</details> +</details> + `.trim(), + ); + }); + it('correctly renders div', () => { expect( serialize( diff --git a/spec/frontend/design_management/utils/cache_update_spec.js b/spec/frontend/design_management/utils/cache_update_spec.js index 7327cf00abd..fa6a666bb37 100644 --- a/spec/frontend/design_management/utils/cache_update_spec.js +++ b/spec/frontend/design_management/utils/cache_update_spec.js @@ -26,11 +26,11 @@ describe('Design Management cache update', () => { describe('error handling', () => { it.each` - fnName | subject | errorMessage | extraArgs - ${'updateStoreAfterDesignsDelete'} | ${updateStoreAfterDesignsDelete} | ${designDeletionError({ singular: true })} | ${[[design]]} - ${'updateStoreAfterAddImageDiffNote'} | ${updateStoreAfterAddImageDiffNote} | ${ADD_IMAGE_DIFF_NOTE_ERROR} | ${[]} - ${'updateStoreAfterUploadDesign'} | ${updateStoreAfterUploadDesign} | ${mockErrors[0]} | ${[]} - ${'updateStoreAfterUpdateImageDiffNote'} | ${updateStoreAfterRepositionImageDiffNote} | ${UPDATE_IMAGE_DIFF_NOTE_ERROR} | ${[]} + fnName | subject | errorMessage | extraArgs + ${'updateStoreAfterDesignsDelete'} | ${updateStoreAfterDesignsDelete} | ${designDeletionError()} | ${[[design]]} + ${'updateStoreAfterAddImageDiffNote'} | ${updateStoreAfterAddImageDiffNote} | ${ADD_IMAGE_DIFF_NOTE_ERROR} | ${[]} + ${'updateStoreAfterUploadDesign'} | ${updateStoreAfterUploadDesign} | ${mockErrors[0]} | ${[]} + ${'updateStoreAfterUpdateImageDiffNote'} | ${updateStoreAfterRepositionImageDiffNote} | ${UPDATE_IMAGE_DIFF_NOTE_ERROR} | ${[]} `('$fnName handles errors in response', ({ subject, extraArgs, errorMessage }) => { expect(createFlash).not.toHaveBeenCalled(); expect(() => subject(mockStore, { errors: mockErrors }, {}, ...extraArgs)).toThrow(); diff --git a/spec/frontend/design_management/utils/error_messages_spec.js b/spec/frontend/design_management/utils/error_messages_spec.js index b80dcd9abde..4994f4f6fd0 100644 --- a/spec/frontend/design_management/utils/error_messages_spec.js +++ b/spec/frontend/design_management/utils/error_messages_spec.js @@ -10,20 +10,21 @@ const mockFilenames = (n) => describe('Error message', () => { describe('designDeletionError', () => { - const singularMsg = 'Could not archive a design. Please try again.'; - const pluralMsg = 'Could not archive designs. Please try again.'; + const singularMsg = 'Failed to archive a design. Please try again.'; + const pluralMsg = 'Failed to archive designs. Please try again.'; - describe('when [singular=true]', () => { - it.each([[undefined], [true]])('uses singular grammar', (singularOption) => { - expect(designDeletionError({ singular: singularOption })).toEqual(singularMsg); - }); - }); - - describe('when [singular=false]', () => { - it('uses plural grammar', () => { - expect(designDeletionError({ singular: false })).toEqual(pluralMsg); - }); - }); + it.each` + designsLength | expectedText + ${undefined} | ${singularMsg} + ${0} | ${pluralMsg} + ${1} | ${singularMsg} + ${2} | ${pluralMsg} + `( + 'returns "$expectedText" when designsLength is $designsLength', + ({ designsLength, expectedText }) => { + expect(designDeletionError(designsLength)).toBe(expectedText); + }, + ); }); describe.each([ @@ -47,12 +48,12 @@ describe('Error message', () => { [ mockFilenames(7), mockFilenames(6), - 'Upload skipped. Some of the designs you tried uploading did not change: 1.jpg, 2.jpg, 3.jpg, 4.jpg, 5.jpg, and 1 more.', + 'Upload skipped. Some of the designs you tried uploading did not change: 1.jpg, 2.jpg, 3.jpg, 4.jpg, 5.jpg and 1 more.', ], [ mockFilenames(8), mockFilenames(7), - 'Upload skipped. Some of the designs you tried uploading did not change: 1.jpg, 2.jpg, 3.jpg, 4.jpg, 5.jpg, and 2 more.', + 'Upload skipped. Some of the designs you tried uploading did not change: 1.jpg, 2.jpg, 3.jpg, 4.jpg, 5.jpg and 2 more.', ], ])('designUploadSkippedWarning', (uploadedFiles, skippedFiles, expected) => { it('returns expected warning message', () => { diff --git a/spec/frontend/fixtures/api_markdown.yml b/spec/frontend/fixtures/api_markdown.yml index 332cc14d1e4..f1db7b5b92f 100644 --- a/spec/frontend/fixtures/api_markdown.yml +++ b/spec/frontend/fixtures/api_markdown.yml @@ -77,6 +77,35 @@ </dd> </dl> +- name: details + markdown: |- + <details> + <summary>Apply this patch</summary> + + ```diff + diff --git a/spec/frontend/fixtures/api_markdown.yml b/spec/frontend/fixtures/api_markdown.yml + index 8433efaf00c..69b12c59d46 100644 + --- a/spec/frontend/fixtures/api_markdown.yml + +++ b/spec/frontend/fixtures/api_markdown.yml + @@ -33,6 +33,13 @@ + * <ruby>漢<rt>ㄏㄢˋ</rt></ruby> + * C<sub>7</sub>H<sub>16</sub> + O<sub>2</sub> → CO<sub>2</sub> + H<sub>2</sub>O + * The **Pythagorean theorem** is often expressed as <var>a<sup>2</sup></var> + <var>b<sup>2</sup></var> = <var>c<sup>2</sup></var>.The **Pythagorean theorem** is often expressed as <var>a<sup>2</sup></var> + <var>b<sup>2</sup></var> = <var>c<sup>2</sup></var> + +- name: details + + markdown: |- + + <details> + + <summary>Apply this patch</summary> + + + + 🐶 much meta, 🐶 many patch + + 🐶 such diff, 🐶 very meme + + 🐶 wow! + + </details> + - name: link + markdown: '[GitLab](https://gitlab.com)' + - name: attachment_link + ``` + + </details> - name: link markdown: '[GitLab](https://gitlab.com)' - name: attachment_link diff --git a/spec/frontend/fixtures/static/oauth_remember_me.html b/spec/frontend/fixtures/static/oauth_remember_me.html index c6af8129b4d..0b4d482925d 100644 --- a/spec/frontend/fixtures/static/oauth_remember_me.html +++ b/spec/frontend/fixtures/static/oauth_remember_me.html @@ -1,22 +1,21 @@ <div id="oauth-container"> -<input id="remember_me" type="checkbox"> + <input id="remember_me" type="checkbox" /> -<form method="post" action="http://example.com/"> - <button class="oauth-login twitter" type="submit"> - <span>Twitter</span> - </button> -</form> + <form method="post" action="http://example.com/"> + <button class="js-oauth-login twitter" type="submit"> + <span>Twitter</span> + </button> + </form> -<form method="post" action="http://example.com/"> - <button class="oauth-login github" type="submit"> - <span>GitHub</span> - </button> -</form> - -<form method="post" action="http://example.com/?redirect_fragment=L1"> - <button class="oauth-login facebook" type="submit"> - <span>Facebook</span> - </button> -</form> + <form method="post" action="http://example.com/"> + <button class="js-oauth-login github" type="submit"> + <span>GitHub</span> + </button> + </form> + <form method="post" action="http://example.com/?redirect_fragment=L1"> + <button class="js-oauth-login facebook" type="submit"> + <span>Facebook</span> + </button> + </form> </div> diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js index 94ad7759110..eb11df2fe43 100644 --- a/spec/frontend/gfm_auto_complete_spec.js +++ b/spec/frontend/gfm_auto_complete_spec.js @@ -1,17 +1,15 @@ /* eslint no-param-reassign: "off" */ import MockAdapter from 'axios-mock-adapter'; import $ from 'jquery'; +import labelsFixture from 'test_fixtures/autocomplete_sources/labels.json'; import GfmAutoComplete, { membersBeforeSave } from 'ee_else_ce/gfm_auto_complete'; import { initEmojiMock } from 'helpers/emoji'; import '~/lib/utils/jquery_at_who'; -import { getJSONFixture } from 'helpers/fixtures'; import { TEST_HOST } from 'helpers/test_constants'; import waitForPromises from 'helpers/wait_for_promises'; import AjaxCache from '~/lib/utils/ajax_cache'; import axios from '~/lib/utils/axios_utils'; -const labelsFixture = getJSONFixture('autocomplete_sources/labels.json'); - describe('GfmAutoComplete', () => { const fetchDataMock = { fetchData: jest.fn() }; let gfmAutoCompleteCallbacks = GfmAutoComplete.prototype.getDefaultCallbacks.call(fetchDataMock); diff --git a/spec/frontend/oauth_remember_me_spec.js b/spec/frontend/oauth_remember_me_spec.js index 70bda1d9f9e..3187cbf6547 100644 --- a/spec/frontend/oauth_remember_me_spec.js +++ b/spec/frontend/oauth_remember_me_spec.js @@ -3,7 +3,7 @@ import OAuthRememberMe from '~/pages/sessions/new/oauth_remember_me'; describe('OAuthRememberMe', () => { const findFormAction = (selector) => { - return $(`#oauth-container .oauth-login${selector}`).parent('form').attr('action'); + return $(`#oauth-container .js-oauth-login${selector}`).parent('form').attr('action'); }; beforeEach(() => { diff --git a/spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js b/spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js index e39a3904613..a29db961452 100644 --- a/spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js +++ b/spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js @@ -44,7 +44,7 @@ describe('preserve_url_fragment', () => { }); it('when "remember-me" is present', () => { - $('.omniauth-btn') + $('.js-oauth-login') .parent('form') .attr('action', (i, href) => `${href}?remember_me=1`); diff --git a/spec/frontend/pipelines/pipeline_multi_actions_spec.js b/spec/frontend/pipelines/pipeline_multi_actions_spec.js index a606595b37d..e24d2e51f08 100644 --- a/spec/frontend/pipelines/pipeline_multi_actions_spec.js +++ b/spec/frontend/pipelines/pipeline_multi_actions_spec.js @@ -95,7 +95,7 @@ describe('Pipeline Multi Actions Dropdown', () => { createComponent({ mockData: { artifacts } }); expect(findFirstArtifactItem().attributes('href')).toBe(artifacts[0].path); - expect(findFirstArtifactItem().text()).toBe(`Download ${artifacts[0].name} artifact`); + expect(findFirstArtifactItem().text()).toBe(artifacts[0].name); }); it('should render empty message when no artifacts are found', () => { diff --git a/spec/frontend/pipelines/pipelines_artifacts_spec.js b/spec/frontend/pipelines/pipelines_artifacts_spec.js index 336255768d7..f33c66dedf3 100644 --- a/spec/frontend/pipelines/pipelines_artifacts_spec.js +++ b/spec/frontend/pipelines/pipelines_artifacts_spec.js @@ -87,8 +87,7 @@ describe('Pipelines Artifacts dropdown', () => { createComponent({ mockData: { artifacts } }); expect(findFirstGlDropdownItem().attributes('href')).toBe(artifacts[0].path); - - expect(findFirstGlDropdownItem().text()).toBe(`Download ${artifacts[0].name} artifact`); + expect(findFirstGlDropdownItem().text()).toBe(artifacts[0].name); }); describe('with a failing request', () => { diff --git a/spec/frontend/ref/components/ref_selector_spec.js b/spec/frontend/ref/components/ref_selector_spec.js index a642a8cf8c2..b486992ac4b 100644 --- a/spec/frontend/ref/components/ref_selector_spec.js +++ b/spec/frontend/ref/components/ref_selector_spec.js @@ -4,6 +4,9 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import { merge, last } from 'lodash'; import Vuex from 'vuex'; +import commit from 'test_fixtures/api/commits/commit.json'; +import branches from 'test_fixtures/api/branches/branches.json'; +import tags from 'test_fixtures/api/tags/tags.json'; import { trimText } from 'helpers/text_helper'; import { ENTER_KEY } from '~/lib/utils/keys'; import { sprintf } from '~/locale'; @@ -21,11 +24,7 @@ const localVue = createLocalVue(); localVue.use(Vuex); describe('Ref selector component', () => { - const fixtures = { - branches: getJSONFixture('api/branches/branches.json'), - tags: getJSONFixture('api/tags/tags.json'), - commit: getJSONFixture('api/commits/commit.json'), - }; + const fixtures = { branches, tags, commit }; const projectId = '8'; @@ -480,8 +479,6 @@ describe('Ref selector component', () => { it('renders each commit as a selectable item with the short SHA and commit title', () => { const dropdownItems = findCommitDropdownItems(); - const { commit } = fixtures; - expect(dropdownItems.at(0).text()).toBe(`${commit.short_id} ${commit.title}`); }); }); diff --git a/spec/frontend/related_merge_requests/components/related_merge_requests_spec.js b/spec/frontend/related_merge_requests/components/related_merge_requests_spec.js index f306fdef624..67f62815720 100644 --- a/spec/frontend/related_merge_requests/components/related_merge_requests_spec.js +++ b/spec/frontend/related_merge_requests/components/related_merge_requests_spec.js @@ -1,23 +1,19 @@ import { mount, createLocalVue } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; +import mockData from 'test_fixtures/issues/related_merge_requests.json'; import axios from '~/lib/utils/axios_utils'; import RelatedMergeRequests from '~/related_merge_requests/components/related_merge_requests.vue'; import createStore from '~/related_merge_requests/store/index'; import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue'; -const FIXTURE_PATH = 'issues/related_merge_requests.json'; const API_ENDPOINT = '/api/v4/projects/2/issues/33/related_merge_requests'; const localVue = createLocalVue(); describe('RelatedMergeRequests', () => { let wrapper; let mock; - let mockData; beforeEach((done) => { - loadFixtures(FIXTURE_PATH); - mockData = getJSONFixture(FIXTURE_PATH); - // put the fixture in DOM as the component expects document.body.innerHTML = `<div id="js-issuable-app"></div>`; document.getElementById('js-issuable-app').dataset.initial = JSON.stringify(mockData); diff --git a/spec/frontend/runner/mock_data.js b/spec/frontend/runner/mock_data.js index c90b9a4c426..b8d0f1273c7 100644 --- a/spec/frontend/runner/mock_data.js +++ b/spec/frontend/runner/mock_data.js @@ -1,14 +1,18 @@ -const runnerFixture = (filename) => getJSONFixture(`graphql/runner/${filename}`); - // Fixtures generated by: spec/frontend/fixtures/runner.rb // Admin queries -export const runnersData = runnerFixture('get_runners.query.graphql.json'); -export const runnersDataPaginated = runnerFixture('get_runners.query.graphql.paginated.json'); -export const runnerData = runnerFixture('get_runner.query.graphql.json'); +import runnersData from 'test_fixtures/graphql/runner/get_runners.query.graphql.json'; +import runnersDataPaginated from 'test_fixtures/graphql/runner/get_runners.query.graphql.paginated.json'; +import runnerData from 'test_fixtures/graphql/runner/get_runner.query.graphql.json'; // Group queries -export const groupRunnersData = runnerFixture('get_group_runners.query.graphql.json'); -export const groupRunnersDataPaginated = runnerFixture( - 'get_group_runners.query.graphql.paginated.json', -); +import groupRunnersData from 'test_fixtures/graphql/runner/get_group_runners.query.graphql.json'; +import groupRunnersDataPaginated from 'test_fixtures/graphql/runner/get_group_runners.query.graphql.paginated.json'; + +export { + runnerData, + runnersDataPaginated, + runnersData, + groupRunnersData, + groupRunnersDataPaginated, +}; diff --git a/spec/frontend/users_select/test_helper.js b/spec/frontend/users_select/test_helper.js index c5adbe9bb09..59edde48eab 100644 --- a/spec/frontend/users_select/test_helper.js +++ b/spec/frontend/users_select/test_helper.js @@ -1,6 +1,7 @@ import MockAdapter from 'axios-mock-adapter'; import { memoize, cloneDeep } from 'lodash'; -import { getFixture, getJSONFixture } from 'helpers/fixtures'; +import usersFixture from 'test_fixtures/autocomplete/users.json'; +import { getFixture } from 'helpers/fixtures'; import waitForPromises from 'helpers/wait_for_promises'; import axios from '~/lib/utils/axios_utils'; import UsersSelect from '~/users_select'; @@ -15,7 +16,7 @@ const getUserSearchHTML = memoize((fixturePath) => { return el.outerHTML; }); -const getUsersFixture = memoize(() => getJSONFixture('autocomplete/users.json')); +const getUsersFixture = () => usersFixture; export const getUsersFixtureAt = (idx) => getUsersFixture()[idx]; diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js index 0bf44fd2177..f0fbb1d5851 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -45,7 +45,7 @@ const createTestMr = (customConfig) => { preferredAutoMergeStrategy: MWPS_MERGE_STRATEGY, availableAutoMergeStrategies: [MWPS_MERGE_STRATEGY], mergeImmediatelyDocsPath: 'path/to/merge/immediately/docs', - transitionStateMachine: () => eventHub.$emit('StateMachineValueChanged', { value: 'value' }), + transitionStateMachine: (transition) => eventHub.$emit('StateMachineValueChanged', transition), translateStateToMachine: () => this.transitionStateMachine(), }; @@ -306,6 +306,9 @@ describe('ReadyToMerge', () => { setImmediate(() => { expect(wrapper.vm.isMakingRequest).toBeTruthy(); expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); + expect(eventHub.$emit).toHaveBeenCalledWith('StateMachineValueChanged', { + transition: 'start-auto-merge', + }); const params = wrapper.vm.service.merge.mock.calls[0][0]; @@ -343,10 +346,15 @@ describe('ReadyToMerge', () => { it('should handle merge action accepted case', (done) => { createComponent(); + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); jest.spyOn(wrapper.vm.service, 'merge').mockReturnValue(returnPromise('success')); jest.spyOn(wrapper.vm, 'initiateMergePolling').mockImplementation(() => {}); wrapper.vm.handleMergeButtonClick(); + expect(eventHub.$emit).toHaveBeenCalledWith('StateMachineValueChanged', { + transition: 'start-merge', + }); + setImmediate(() => { expect(wrapper.vm.isMakingRequest).toBeTruthy(); expect(wrapper.vm.initiateMergePolling).toHaveBeenCalled(); diff --git a/spec/helpers/startupjs_helper_spec.rb b/spec/helpers/startupjs_helper_spec.rb index 6d61c38d4a5..8d429b59291 100644 --- a/spec/helpers/startupjs_helper_spec.rb +++ b/spec/helpers/startupjs_helper_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe StartupjsHelper do + using RSpec::Parameterized::TableSyntax + describe '#page_startup_graphql_calls' do let(:query_location) { 'repository/path_last_commit' } let(:query_content) do @@ -17,4 +19,24 @@ RSpec.describe StartupjsHelper do expect(startup_graphql_calls).to include({ query: query_content, variables: { ref: 'foo' } }) end end + + describe '#page_startup_graphql_headers' do + where(:csrf_token, :feature_category, :expected) do + 'abc' | 'web_ide' | { 'X-CSRF-Token' => 'abc', 'x-gitlab-feature-category' => 'web_ide' } + '' | '' | { 'X-CSRF-Token' => '', 'x-gitlab-feature-category' => '' } + 'abc' | nil | { 'X-CSRF-Token' => 'abc', 'x-gitlab-feature-category' => '' } + 'something' | ' ' | { 'X-CSRF-Token' => 'something', 'x-gitlab-feature-category' => '' } + end + + with_them do + before do + allow(helper).to receive(:form_authenticity_token).and_return(csrf_token) + ::Gitlab::ApplicationContext.push(feature_category: feature_category) + end + + it 'returns hash of headers for GraphQL requests' do + expect(helper.page_startup_graphql_headers).to eq(expected) + end + end + end end diff --git a/spec/lib/gitlab/database/count_spec.rb b/spec/lib/gitlab/database/count_spec.rb index d65413c2a00..e712ad09927 100644 --- a/spec/lib/gitlab/database/count_spec.rb +++ b/spec/lib/gitlab/database/count_spec.rb @@ -46,5 +46,49 @@ RSpec.describe Gitlab::Database::Count do subject end end + + context 'default strategies' do + subject { described_class.approximate_counts(models) } + + context 'with a read-only database' do + before do + allow(Gitlab::Database).to receive(:read_only?).and_return(true) + end + + it 'only uses the ExactCountStrategy' do + allow_next_instance_of(Gitlab::Database::Count::TablesampleCountStrategy) do |instance| + expect(instance).not_to receive(:count) + end + allow_next_instance_of(Gitlab::Database::Count::ReltuplesCountStrategy) do |instance| + expect(instance).not_to receive(:count) + end + expect_next_instance_of(Gitlab::Database::Count::ExactCountStrategy) do |instance| + expect(instance).to receive(:count).and_return({}) + end + + subject + end + end + + context 'with a read-write database' do + before do + allow(Gitlab::Database).to receive(:read_only?).and_return(false) + end + + it 'uses the available strategies' do + [ + Gitlab::Database::Count::TablesampleCountStrategy, + Gitlab::Database::Count::ReltuplesCountStrategy, + Gitlab::Database::Count::ExactCountStrategy + ].each do |strategy_klass| + expect_next_instance_of(strategy_klass) do |instance| + expect(instance).to receive(:count).and_return({}) + end + end + + subject + end + end + end end end |