Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
e7527f5486
commit
59e6c2df22
|
@ -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.
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -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'),
|
||||
};
|
||||
},
|
||||
});
|
|
@ -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'),
|
||||
};
|
||||
},
|
||||
});
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
||||
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) {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -26,14 +26,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.omniauth-btn {
|
||||
width: 48%;
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.decline-page {
|
||||
width: 350px;
|
||||
}
|
||||
|
|
|
@ -99,11 +99,6 @@
|
|||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
margin-bottom: $gl-padding;
|
||||
}
|
||||
|
||||
.omniauth-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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 => ({
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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**.
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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'} | ${{}}
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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());
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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());
|
||||
});
|
||||
});
|
|
@ -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(
|
||||
|
|
|
@ -27,7 +27,7 @@ describe('Design Management cache update', () => {
|
|||
describe('error handling', () => {
|
||||
it.each`
|
||||
fnName | subject | errorMessage | extraArgs
|
||||
${'updateStoreAfterDesignsDelete'} | ${updateStoreAfterDesignsDelete} | ${designDeletionError({ singular: true })} | ${[[design]]}
|
||||
${'updateStoreAfterDesignsDelete'} | ${updateStoreAfterDesignsDelete} | ${designDeletionError()} | ${[[design]]}
|
||||
${'updateStoreAfterAddImageDiffNote'} | ${updateStoreAfterAddImageDiffNote} | ${ADD_IMAGE_DIFF_NOTE_ERROR} | ${[]}
|
||||
${'updateStoreAfterUploadDesign'} | ${updateStoreAfterUploadDesign} | ${mockErrors[0]} | ${[]}
|
||||
${'updateStoreAfterUpdateImageDiffNote'} | ${updateStoreAfterRepositionImageDiffNote} | ${UPDATE_IMAGE_DIFF_NOTE_ERROR} | ${[]}
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
<form method="post" action="http://example.com/">
|
||||
<button class="js-oauth-login twitter" type="submit">
|
||||
<span>Twitter</span>
|
||||
</button>
|
||||
</form>
|
||||
</form>
|
||||
|
||||
<form method="post" action="http://example.com/">
|
||||
<button class="oauth-login github" type="submit">
|
||||
<form method="post" action="http://example.com/">
|
||||
<button class="js-oauth-login github" type="submit">
|
||||
<span>GitHub</span>
|
||||
</button>
|
||||
</form>
|
||||
</form>
|
||||
|
||||
<form method="post" action="http://example.com/?redirect_fragment=L1">
|
||||
<button class="oauth-login facebook" type="submit">
|
||||
<form method="post" action="http://example.com/?redirect_fragment=L1">
|
||||
<button class="js-oauth-login facebook" type="submit">
|
||||
<span>Facebook</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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`);
|
||||
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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}`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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];
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue