Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-10-08 21:09:48 +00:00
parent e7527f5486
commit 59e6c2df22
59 changed files with 851 additions and 230 deletions

View File

@ -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.

View File

@ -111,6 +111,15 @@ export default {
:label="__('Add a numbered list')"
@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"

View File

@ -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>

View File

@ -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'),
};
},
});

View File

@ -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'),
};
},
});

View File

@ -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,

View File

@ -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;

View File

@ -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);

View File

@ -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() {

View File

@ -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);

View File

@ -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);

View File

@ -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');

View File

@ -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>

View File

@ -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>

View File

@ -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) {

View File

@ -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 = {

View File

@ -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 {

View File

@ -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;

View File

@ -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;
}

View File

@ -26,14 +26,6 @@
}
}
.omniauth-btn {
width: 48%;
@include media-breakpoint-down(md) {
width: 100%;
}
}
.decline-page {
width: 350px;
}

View File

@ -99,11 +99,6 @@
padding: 0;
border: 0;
background: none;
margin-bottom: $gl-padding;
}
.omniauth-btn {
width: 100%;
}
}

View File

@ -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);

View File

@ -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";

View File

@ -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);
}
}
}

View File

@ -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")

View File

@ -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

View File

@ -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')

View File

@ -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)

View File

@ -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

View File

@ -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")

View File

@ -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 => ({

View File

@ -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.

View File

@ -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**.

View File

@ -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
}
}
}
}
}
```

View File

@ -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:

View File

@ -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

View File

@ -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)

View File

@ -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 ""

View File

@ -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'} | ${{}}

View File

@ -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');
});
});

View File

@ -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());
});
});
});

View File

@ -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());
});
});

View File

@ -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(

View File

@ -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();

View File

@ -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', () => {

View File

@ -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

View File

@ -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>

View File

@ -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);

View File

@ -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(() => {

View File

@ -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`);

View File

@ -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', () => {

View File

@ -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', () => {

View File

@ -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}`);
});
});

View File

@ -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);

View File

@ -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,
};

View File

@ -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];

View File

@ -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();

View File

@ -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

View File

@ -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