Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-10-09 00:09:48 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-10-09 00:09:48 +0300
commit59e6c2df22c69baa791529db3326e68c9de10b54 (patch)
treeaa75309a037a6031c38f8ccd9afe53cbcd519355
parente7527f548681e4f9efd32f9c3da937ad74c68948 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/merge_request_templates/Deprecations.md2
-rw-r--r--app/assets/javascripts/content_editor/components/top_toolbar.vue9
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/details.vue33
-rw-r--r--app/assets/javascripts/content_editor/extensions/details.js36
-rw-r--r--app/assets/javascripts/content_editor/extensions/details_content.js25
-rw-r--r--app/assets/javascripts/content_editor/services/create_content_editor.js4
-rw-r--r--app/assets/javascripts/content_editor/services/markdown_serializer.js12
-rw-r--r--app/assets/javascripts/design_management/pages/design/index.vue2
-rw-r--r--app/assets/javascripts/design_management/pages/index.vue2
-rw-r--r--app/assets/javascripts/design_management/utils/cache_update.js2
-rw-r--r--app/assets/javascripts/design_management/utils/error_messages.js72
-rw-r--r--app/assets/javascripts/pages/sessions/new/oauth_remember_me.js2
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue7
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue14
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/constants.js11
-rw-r--r--app/assets/stylesheets/components/content_editor.scss36
-rw-r--r--app/assets/stylesheets/framework/forms.scss8
-rw-r--r--app/assets/stylesheets/framework/typography.scss10
-rw-r--r--app/assets/stylesheets/page_bundles/signup.scss8
-rw-r--r--app/assets/stylesheets/pages/login.scss5
-rw-r--r--app/assets/stylesheets/startup/startup-dark.scss6
-rw-r--r--app/assets/stylesheets/startup/startup-signin.scss44
-rw-r--r--app/assets/stylesheets/themes/theme_helper.scss4
-rw-r--r--app/helpers/startupjs_helper.rb7
-rw-r--r--app/views/devise/sessions/_new_base.html.haml6
-rw-r--r--app/views/devise/shared/_omniauth_box.html.haml10
-rw-r--r--app/views/devise/shared/_signup_omniauth_provider_list.haml8
-rw-r--r--app/views/devise/shared/_signup_omniauth_providers.haml2
-rw-r--r--app/views/devise/shared/_signup_omniauth_providers_top.haml2
-rw-r--r--app/views/layouts/_startup_js.html.haml5
-rw-r--r--doc/development/documentation/styleguide/index.md10
-rw-r--r--doc/user/group/import/index.md2
-rw-r--r--doc/user/project/repository/branches/default.md29
-rw-r--r--doc/user/usage_quotas.md32
-rw-r--r--lib/gitlab/database/count.rb12
-rw-r--r--lib/tasks/rubocop.rake3
-rw-r--r--locale/gitlab.pot38
-rw-r--r--spec/frontend/content_editor/components/top_toolbar_spec.js1
-rw-r--r--spec/frontend/content_editor/components/wrappers/details_spec.js40
-rw-r--r--spec/frontend/content_editor/extensions/details_content_spec.js76
-rw-r--r--spec/frontend/content_editor/extensions/details_spec.js92
-rw-r--r--spec/frontend/content_editor/services/markdown_serializer_spec.js107
-rw-r--r--spec/frontend/design_management/utils/cache_update_spec.js10
-rw-r--r--spec/frontend/design_management/utils/error_messages_spec.js31
-rw-r--r--spec/frontend/fixtures/api_markdown.yml29
-rw-r--r--spec/frontend/fixtures/static/oauth_remember_me.html33
-rw-r--r--spec/frontend/gfm_auto_complete_spec.js4
-rw-r--r--spec/frontend/oauth_remember_me_spec.js2
-rw-r--r--spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js2
-rw-r--r--spec/frontend/pipelines/pipeline_multi_actions_spec.js2
-rw-r--r--spec/frontend/pipelines/pipelines_artifacts_spec.js3
-rw-r--r--spec/frontend/ref/components/ref_selector_spec.js11
-rw-r--r--spec/frontend/related_merge_requests/components/related_merge_requests_spec.js6
-rw-r--r--spec/frontend/runner/mock_data.js22
-rw-r--r--spec/frontend/users_select/test_helper.js5
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js10
-rw-r--r--spec/helpers/startupjs_helper_spec.rb22
-rw-r--r--spec/lib/gitlab/database/count_spec.rb44
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