Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
69944ffb68
commit
6d31b8f052
58 changed files with 1136 additions and 629 deletions
|
@ -113,10 +113,9 @@ const Api = {
|
|||
.get(url, {
|
||||
params: Object.assign(defaults, options),
|
||||
})
|
||||
.then(({ data }) => {
|
||||
.then(({ data, headers }) => {
|
||||
callback(data);
|
||||
|
||||
return data;
|
||||
return { data, headers };
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -47,7 +47,8 @@ export default {
|
|||
hasSearchQuery: true,
|
||||
});
|
||||
},
|
||||
[types.RECEIVE_SEARCHED_ITEMS_SUCCESS](state, rawItems) {
|
||||
[types.RECEIVE_SEARCHED_ITEMS_SUCCESS](state, results) {
|
||||
const rawItems = results.data;
|
||||
Object.assign(state, {
|
||||
items: rawItems.map(rawItem => ({
|
||||
id: rawItem.id,
|
||||
|
|
|
@ -11,11 +11,35 @@ export default {
|
|||
computed: {
|
||||
...mapState(['traceEndpoint', 'trace', 'isTraceComplete']),
|
||||
},
|
||||
updated() {
|
||||
this.$nextTick(() => {
|
||||
this.handleScrollDown();
|
||||
});
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.handleScrollDown();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['toggleCollapsibleLine']),
|
||||
...mapActions(['toggleCollapsibleLine', 'scrollBottom']),
|
||||
handleOnClickCollapsibleLine(section) {
|
||||
this.toggleCollapsibleLine(section);
|
||||
},
|
||||
/**
|
||||
* The job log is sent in HTML, which means we need to use `v-html` to render it
|
||||
* Using the updated hook with $nextTick is not enough to wait for the DOM to be updated
|
||||
* in this case because it runs before `v-html` has finished running, since there's no
|
||||
* Vue binding.
|
||||
* In order to scroll the page down after `v-html` has finished, we need to use setTimeout
|
||||
*/
|
||||
handleScrollDown() {
|
||||
if (this.isScrolledToBottomBeforeReceivingTrace) {
|
||||
setTimeout(() => {
|
||||
this.scrollBottom();
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
import _ from 'underscore';
|
||||
|
||||
import { s__, __, sprintf } from '~/locale';
|
||||
import { truncateSha } from '~/lib/utils/text_utility';
|
||||
|
||||
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
|
||||
import noteEditedText from './note_edited_text.vue';
|
||||
import noteHeader from './note_header.vue';
|
||||
|
||||
export default {
|
||||
name: 'DiffDiscussionHeader',
|
||||
components: {
|
||||
userAvatarLink,
|
||||
noteEditedText,
|
||||
noteHeader,
|
||||
},
|
||||
props: {
|
||||
discussion: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
notes() {
|
||||
return this.discussion.notes;
|
||||
},
|
||||
firstNote() {
|
||||
return this.notes[0];
|
||||
},
|
||||
lastNote() {
|
||||
return this.notes[this.notes.length - 1];
|
||||
},
|
||||
author() {
|
||||
return this.firstNote.author;
|
||||
},
|
||||
resolvedText() {
|
||||
return this.discussion.resolved_by_push ? __('Automatically resolved') : __('Resolved');
|
||||
},
|
||||
lastUpdatedBy() {
|
||||
return this.notes.length > 1 ? this.lastNote.author : null;
|
||||
},
|
||||
lastUpdatedAt() {
|
||||
return this.notes.length > 1 ? this.lastNote.created_at : null;
|
||||
},
|
||||
headerText() {
|
||||
const linkStart = `<a href="${_.escape(this.discussion.discussion_path)}">`;
|
||||
const linkEnd = '</a>';
|
||||
|
||||
const { commit_id: commitId } = this.discussion;
|
||||
let commitDisplay = commitId;
|
||||
|
||||
if (commitId) {
|
||||
commitDisplay = `<span class="commit-sha">${truncateSha(commitId)}</span>`;
|
||||
}
|
||||
|
||||
const {
|
||||
for_commit: isForCommit,
|
||||
diff_discussion: isDiffDiscussion,
|
||||
active: isActive,
|
||||
} = this.discussion;
|
||||
|
||||
let text = s__('MergeRequests|started a thread');
|
||||
if (isForCommit) {
|
||||
text = s__(
|
||||
'MergeRequests|started a thread on commit %{linkStart}%{commitDisplay}%{linkEnd}',
|
||||
);
|
||||
} else if (isDiffDiscussion && commitId) {
|
||||
text = isActive
|
||||
? s__('MergeRequests|started a thread on commit %{linkStart}%{commitDisplay}%{linkEnd}')
|
||||
: s__(
|
||||
'MergeRequests|started a thread on an outdated change in commit %{linkStart}%{commitDisplay}%{linkEnd}',
|
||||
);
|
||||
} else if (isDiffDiscussion) {
|
||||
text = isActive
|
||||
? s__('MergeRequests|started a thread on %{linkStart}the diff%{linkEnd}')
|
||||
: s__(
|
||||
'MergeRequests|started a thread on %{linkStart}an old version of the diff%{linkEnd}',
|
||||
);
|
||||
}
|
||||
|
||||
return sprintf(text, { commitDisplay, linkStart, linkEnd }, false);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['toggleDiscussion']),
|
||||
toggleDiscussionHandler() {
|
||||
this.toggleDiscussion({ discussionId: this.discussion.id });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="discussion-header note-wrapper">
|
||||
<div v-once class="timeline-icon align-self-start flex-shrink-0">
|
||||
<user-avatar-link
|
||||
v-if="author"
|
||||
:link-href="author.path"
|
||||
:img-src="author.avatar_url"
|
||||
:img-alt="author.name"
|
||||
:img-size="40"
|
||||
/>
|
||||
</div>
|
||||
<div class="timeline-content w-100">
|
||||
<note-header
|
||||
:author="author"
|
||||
:created-at="firstNote.created_at"
|
||||
:note-id="firstNote.id"
|
||||
:include-toggle="true"
|
||||
:expanded="discussion.expanded"
|
||||
@toggleHandler="toggleDiscussionHandler"
|
||||
>
|
||||
<span v-html="headerText"></span>
|
||||
</note-header>
|
||||
<note-edited-text
|
||||
v-if="discussion.resolved"
|
||||
:edited-at="discussion.resolved_at"
|
||||
:edited-by="discussion.resolved_by"
|
||||
:action-text="resolvedText"
|
||||
class-name="discussion-headline-light js-discussion-headline"
|
||||
/>
|
||||
<note-edited-text
|
||||
v-else-if="lastUpdatedAt"
|
||||
:edited-at="lastUpdatedAt"
|
||||
:edited-by="lastUpdatedBy"
|
||||
:action-text="__('Last updated')"
|
||||
class-name="discussion-headline-light js-discussion-headline"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,18 +1,15 @@
|
|||
<script>
|
||||
import _ from 'underscore';
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import { GlTooltipDirective } from '@gitlab/ui';
|
||||
import { truncateSha } from '~/lib/utils/text_utility';
|
||||
import { s__, __, sprintf } from '~/locale';
|
||||
import { s__, __ } from '~/locale';
|
||||
import { clearDraft, getDiscussionReplyKey } from '~/lib/utils/autosave';
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import diffLineNoteFormMixin from 'ee_else_ce/notes/mixins/diff_line_note_form';
|
||||
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
|
||||
import Flash from '../../flash';
|
||||
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
|
||||
import noteHeader from './note_header.vue';
|
||||
import diffDiscussionHeader from './diff_discussion_header.vue';
|
||||
import noteSignedOutWidget from './note_signed_out_widget.vue';
|
||||
import noteEditedText from './note_edited_text.vue';
|
||||
import noteForm from './note_form.vue';
|
||||
import diffWithNote from './diff_with_note.vue';
|
||||
import noteable from '../mixins/noteable';
|
||||
|
@ -27,9 +24,8 @@ export default {
|
|||
components: {
|
||||
icon,
|
||||
userAvatarLink,
|
||||
noteHeader,
|
||||
diffDiscussionHeader,
|
||||
noteSignedOutWidget,
|
||||
noteEditedText,
|
||||
noteForm,
|
||||
DraftNote: () => import('ee_component/batch_comments/components/draft_note.vue'),
|
||||
TimelineEntryItem,
|
||||
|
@ -92,9 +88,6 @@ export default {
|
|||
currentUser() {
|
||||
return this.getUserData;
|
||||
},
|
||||
author() {
|
||||
return this.firstNote.author;
|
||||
},
|
||||
autosaveKey() {
|
||||
return getDiscussionReplyKey(this.firstNote.noteable_type, this.discussion.id);
|
||||
},
|
||||
|
@ -104,27 +97,6 @@ export default {
|
|||
firstNote() {
|
||||
return this.discussion.notes.slice(0, 1)[0];
|
||||
},
|
||||
lastUpdatedBy() {
|
||||
const { notes } = this.discussion;
|
||||
|
||||
if (notes.length > 1) {
|
||||
return notes[notes.length - 1].author;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
lastUpdatedAt() {
|
||||
const { notes } = this.discussion;
|
||||
|
||||
if (notes.length > 1) {
|
||||
return notes[notes.length - 1].created_at;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
resolvedText() {
|
||||
return this.discussion.resolved_by_push ? __('Automatically resolved') : __('Resolved');
|
||||
},
|
||||
shouldShowJumpToNextDiscussion() {
|
||||
return this.showJumpToNextDiscussion(this.discussionsByDiffOrder ? 'diff' : 'discussion');
|
||||
},
|
||||
|
@ -150,40 +122,6 @@ export default {
|
|||
shouldHideDiscussionBody() {
|
||||
return this.shouldRenderDiffs && !this.isExpanded;
|
||||
},
|
||||
actionText() {
|
||||
const linkStart = `<a href="${_.escape(this.discussion.discussion_path)}">`;
|
||||
const linkEnd = '</a>';
|
||||
|
||||
let { commit_id: commitId } = this.discussion;
|
||||
if (commitId) {
|
||||
commitId = `<span class="commit-sha">${truncateSha(commitId)}</span>`;
|
||||
}
|
||||
|
||||
const {
|
||||
for_commit: isForCommit,
|
||||
diff_discussion: isDiffDiscussion,
|
||||
active: isActive,
|
||||
} = this.discussion;
|
||||
|
||||
let text = s__('MergeRequests|started a thread');
|
||||
if (isForCommit) {
|
||||
text = s__('MergeRequests|started a thread on commit %{linkStart}%{commitId}%{linkEnd}');
|
||||
} else if (isDiffDiscussion && commitId) {
|
||||
text = isActive
|
||||
? s__('MergeRequests|started a thread on commit %{linkStart}%{commitId}%{linkEnd}')
|
||||
: s__(
|
||||
'MergeRequests|started a thread on an outdated change in commit %{linkStart}%{commitId}%{linkEnd}',
|
||||
);
|
||||
} else if (isDiffDiscussion) {
|
||||
text = isActive
|
||||
? s__('MergeRequests|started a thread on %{linkStart}the diff%{linkEnd}')
|
||||
: s__(
|
||||
'MergeRequests|started a thread on %{linkStart}an old version of the diff%{linkEnd}',
|
||||
);
|
||||
}
|
||||
|
||||
return sprintf(text, { commitId, linkStart, linkEnd }, false);
|
||||
},
|
||||
diffLine() {
|
||||
if (this.line) {
|
||||
return this.line;
|
||||
|
@ -208,16 +146,11 @@ export default {
|
|||
methods: {
|
||||
...mapActions([
|
||||
'saveNote',
|
||||
'toggleDiscussion',
|
||||
'removePlaceholderNotes',
|
||||
'toggleResolveNote',
|
||||
'expandDiscussion',
|
||||
'removeConvertedDiscussion',
|
||||
]),
|
||||
truncateSha,
|
||||
toggleDiscussionHandler() {
|
||||
this.toggleDiscussion({ discussionId: this.discussion.id });
|
||||
},
|
||||
showReplyForm() {
|
||||
this.isReplying = true;
|
||||
},
|
||||
|
@ -311,43 +244,7 @@ export default {
|
|||
class="discussion js-discussion-container"
|
||||
data-qa-selector="discussion_content"
|
||||
>
|
||||
<div v-if="shouldRenderDiffs" class="discussion-header note-wrapper">
|
||||
<div v-once class="timeline-icon align-self-start flex-shrink-0">
|
||||
<user-avatar-link
|
||||
v-if="author"
|
||||
:link-href="author.path"
|
||||
:img-src="author.avatar_url"
|
||||
:img-alt="author.name"
|
||||
:img-size="40"
|
||||
/>
|
||||
</div>
|
||||
<div class="timeline-content w-100">
|
||||
<note-header
|
||||
:author="author"
|
||||
:created-at="firstNote.created_at"
|
||||
:note-id="firstNote.id"
|
||||
:include-toggle="true"
|
||||
:expanded="discussion.expanded"
|
||||
@toggleHandler="toggleDiscussionHandler"
|
||||
>
|
||||
<span v-html="actionText"></span>
|
||||
</note-header>
|
||||
<note-edited-text
|
||||
v-if="discussion.resolved"
|
||||
:edited-at="discussion.resolved_at"
|
||||
:edited-by="discussion.resolved_by"
|
||||
:action-text="resolvedText"
|
||||
class-name="discussion-headline-light js-discussion-headline"
|
||||
/>
|
||||
<note-edited-text
|
||||
v-else-if="lastUpdatedAt"
|
||||
:edited-at="lastUpdatedAt"
|
||||
:edited-by="lastUpdatedBy"
|
||||
action-text="Last updated"
|
||||
class-name="discussion-headline-light js-discussion-headline"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<diff-discussion-header v-if="shouldRenderDiffs" :discussion="discussion" />
|
||||
<div v-if="!shouldHideDiscussionBody" class="discussion-body">
|
||||
<component
|
||||
:is="wrapperComponent"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import { GlButton, GlFormInput, GlFormGroup } from '@gitlab/ui';
|
||||
import _ from 'underscore';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
|
||||
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
|
||||
|
@ -23,6 +24,7 @@ export default {
|
|||
'markdownDocsPath',
|
||||
'markdownPreviewPath',
|
||||
'releasesPagePath',
|
||||
'updateReleaseApiDocsPath',
|
||||
]),
|
||||
showForm() {
|
||||
return !this.isFetchingRelease && !this.fetchError;
|
||||
|
@ -42,6 +44,20 @@ export default {
|
|||
tagName() {
|
||||
return this.$store.state.release.tagName;
|
||||
},
|
||||
tagNameHintText() {
|
||||
return sprintf(
|
||||
__(
|
||||
'Changing a Release tag is only supported via Releases API. %{linkStart}More information%{linkEnd}',
|
||||
),
|
||||
{
|
||||
linkStart: `<a href="${_.escape(
|
||||
this.updateReleaseApiDocsPath,
|
||||
)}" target="_blank" rel="noopener noreferrer">`,
|
||||
linkEnd: '</a>',
|
||||
},
|
||||
false,
|
||||
);
|
||||
},
|
||||
releaseTitle: {
|
||||
get() {
|
||||
return this.$store.state.release.name;
|
||||
|
@ -77,22 +93,22 @@ export default {
|
|||
<div class="d-flex flex-column">
|
||||
<p class="pt-3 js-subtitle-text" v-html="subtitleText"></p>
|
||||
<form v-if="showForm" @submit.prevent="updateRelease()">
|
||||
<div class="row">
|
||||
<gl-form-group class="col-md-6 col-lg-5 col-xl-4">
|
||||
<label for="git-ref">{{ __('Tag name') }}</label>
|
||||
<gl-form-input
|
||||
id="git-ref"
|
||||
v-model="tagName"
|
||||
type="text"
|
||||
class="form-control"
|
||||
aria-describedby="tag-name-help"
|
||||
disabled
|
||||
/>
|
||||
<div id="tag-name-help" class="form-text text-muted">
|
||||
{{ __('Choose an existing tag, or create a new one') }}
|
||||
<gl-form-group>
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-lg-5 col-xl-4">
|
||||
<label for="git-ref">{{ __('Tag name') }}</label>
|
||||
<gl-form-input
|
||||
id="git-ref"
|
||||
v-model="tagName"
|
||||
type="text"
|
||||
class="form-control"
|
||||
aria-describedby="tag-name-help"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</gl-form-group>
|
||||
</div>
|
||||
</div>
|
||||
<div id="tag-name-help" class="form-text text-muted" v-html="tagNameHintText"></div>
|
||||
</gl-form-group>
|
||||
<gl-form-group>
|
||||
<label for="release-title">{{ __('Release title') }}</label>
|
||||
<gl-form-input
|
||||
|
|
|
@ -4,6 +4,7 @@ export default () => ({
|
|||
releasesPagePath: null,
|
||||
markdownDocsPath: null,
|
||||
markdownPreviewPath: null,
|
||||
updateReleaseApiDocsPath: null,
|
||||
|
||||
release: null,
|
||||
|
||||
|
|
|
@ -16,7 +16,6 @@ export default function setupVueRepositoryList() {
|
|||
const { dataset } = el;
|
||||
const { projectPath, projectShortPath, ref, fullName } = dataset;
|
||||
const router = createRouter(projectPath, ref);
|
||||
const hideOnRootEls = document.querySelectorAll('.js-hide-on-root');
|
||||
|
||||
apolloProvider.clients.defaultClient.cache.writeData({
|
||||
data: {
|
||||
|
@ -28,20 +27,7 @@ export default function setupVueRepositoryList() {
|
|||
});
|
||||
|
||||
router.afterEach(({ params: { pathMatch } }) => {
|
||||
const isRoot = pathMatch === undefined || pathMatch === '/';
|
||||
|
||||
setTitle(pathMatch, ref, fullName);
|
||||
|
||||
if (!isRoot) {
|
||||
document
|
||||
.querySelectorAll('.js-keep-hidden-on-navigation')
|
||||
.forEach(elem => elem.classList.add('hidden'));
|
||||
}
|
||||
|
||||
document
|
||||
.querySelectorAll('.js-hide-on-navigation')
|
||||
.forEach(elem => elem.classList.toggle('hidden', !isRoot));
|
||||
hideOnRootEls.forEach(elem => elem.classList.toggle('hidden', isRoot));
|
||||
});
|
||||
|
||||
const breadcrumbEl = document.getElementById('js-repo-breadcrumb');
|
||||
|
|
|
@ -1,13 +1,25 @@
|
|||
<script>
|
||||
import TreeContent from '../components/tree_content.vue';
|
||||
import TreePage from './tree.vue';
|
||||
import { updateElementsVisibility } from '../utils/dom';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TreeContent,
|
||||
TreePage,
|
||||
},
|
||||
mounted() {
|
||||
this.updateProjectElements(true);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.updateProjectElements(false);
|
||||
},
|
||||
methods: {
|
||||
updateProjectElements(isShow) {
|
||||
updateElementsVisibility('.js-show-on-project-root', isShow);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tree-content />
|
||||
<tree-page path="/" />
|
||||
</template>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import TreeContent from '../components/tree_content.vue';
|
||||
import { updateElementsVisibility } from '../utils/dom';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -12,6 +13,23 @@ export default {
|
|||
default: '/',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isRoot() {
|
||||
return this.path === '/';
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
isRoot: {
|
||||
immediate: true,
|
||||
handler: 'updateElements',
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateElements(isRoot) {
|
||||
updateElementsVisibility('.js-show-on-root', isRoot);
|
||||
updateElementsVisibility('.js-hide-on-root', !isRoot);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
4
app/assets/javascripts/repository/utils/dom.js
Normal file
4
app/assets/javascripts/repository/utils/dom.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const updateElementsVisibility = (selector, isVisible) => {
|
||||
document.querySelectorAll(selector).forEach(elem => elem.classList.toggle('hidden', !isVisible));
|
||||
};
|
|
@ -1,10 +1,14 @@
|
|||
const DEFAULT_TITLE = '· GitLab';
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const setTitle = (pathMatch, ref, project) => {
|
||||
if (!pathMatch) return;
|
||||
if (!pathMatch) {
|
||||
document.title = `${project} ${DEFAULT_TITLE}`;
|
||||
return;
|
||||
}
|
||||
|
||||
const path = pathMatch.replace(/^\//, '');
|
||||
const isEmpty = path === '';
|
||||
|
||||
/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
|
||||
document.title = `${isEmpty ? 'Files' : path} · ${ref} · ${project}`;
|
||||
document.title = `${isEmpty ? 'Files' : path} · ${ref} · ${project} ${DEFAULT_TITLE}`;
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import _ from 'underscore';
|
||||
import { GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
|
||||
import { GlLoadingIcon, GlSearchBoxByType, GlInfiniteScroll } from '@gitlab/ui';
|
||||
import ProjectListItem from './project_list_item.vue';
|
||||
|
||||
const SEARCH_INPUT_TIMEOUT_MS = 500;
|
||||
|
@ -10,6 +10,7 @@ export default {
|
|||
components: {
|
||||
GlLoadingIcon,
|
||||
GlSearchBoxByType,
|
||||
GlInfiniteScroll,
|
||||
ProjectListItem,
|
||||
},
|
||||
props: {
|
||||
|
@ -41,6 +42,11 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
totalResults: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -51,6 +57,9 @@ export default {
|
|||
projectClicked(project) {
|
||||
this.$emit('projectClicked', project);
|
||||
},
|
||||
bottomReached() {
|
||||
this.$emit('bottomReached');
|
||||
},
|
||||
isSelected(project) {
|
||||
return Boolean(_.find(this.selectedProjects, { id: project.id }));
|
||||
},
|
||||
|
@ -71,18 +80,25 @@ export default {
|
|||
@input="onInput"
|
||||
/>
|
||||
<div class="d-flex flex-column">
|
||||
<gl-loading-icon v-if="showLoadingIndicator" :size="2" class="py-2 px-4" />
|
||||
<div v-if="!showLoadingIndicator" class="d-flex flex-column">
|
||||
<project-list-item
|
||||
v-for="project in projectSearchResults"
|
||||
:key="project.id"
|
||||
:selected="isSelected(project)"
|
||||
:project="project"
|
||||
:matcher="searchQuery"
|
||||
class="js-project-list-item"
|
||||
@click="projectClicked(project)"
|
||||
/>
|
||||
</div>
|
||||
<gl-loading-icon v-if="showLoadingIndicator" :size="1" class="py-2 px-4" />
|
||||
<gl-infinite-scroll
|
||||
:max-list-height="402"
|
||||
:fetched-items="projectSearchResults.length"
|
||||
:total-items="totalResults"
|
||||
@bottomReached="bottomReached"
|
||||
>
|
||||
<div v-if="!showLoadingIndicator" slot="items" class="d-flex flex-column">
|
||||
<project-list-item
|
||||
v-for="project in projectSearchResults"
|
||||
:key="project.id"
|
||||
:selected="isSelected(project)"
|
||||
:project="project"
|
||||
:matcher="searchQuery"
|
||||
class="js-project-list-item"
|
||||
@click="projectClicked(project)"
|
||||
/>
|
||||
</div>
|
||||
</gl-infinite-scroll>
|
||||
<div v-if="showNoResultsMessage" class="text-muted ml-2 js-no-results-message">
|
||||
{{ __('Sorry, no projects matched your search') }}
|
||||
</div>
|
||||
|
|
|
@ -297,9 +297,7 @@ module ApplicationSettingsHelper
|
|||
:snowplow_iglu_registry_url,
|
||||
:push_event_hooks_limit,
|
||||
:push_event_activities_limit,
|
||||
:custom_http_clone_url_root,
|
||||
:pendo_enabled,
|
||||
:pendo_url
|
||||
:custom_http_clone_url_root
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
@ -26,7 +26,8 @@ module ReleasesHelper
|
|||
tag_name: @release.tag,
|
||||
markdown_preview_path: preview_markdown_path(@project),
|
||||
markdown_docs_path: help_page_path('user/markdown'),
|
||||
releases_page_path: project_releases_path(@project, anchor: @release.tag)
|
||||
releases_page_path: project_releases_path(@project, anchor: @release.tag),
|
||||
update_release_api_docs_path: help_page_path('api/releases/index.md', anchor: 'update-a-release')
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,7 +4,7 @@ module RepositoryLanguagesHelper
|
|||
def repository_languages_bar(languages)
|
||||
return if languages.none?
|
||||
|
||||
content_tag :div, class: 'progress repository-languages-bar' do
|
||||
content_tag :div, class: 'progress repository-languages-bar js-show-on-project-root' do
|
||||
safe_join(languages.map { |lang| language_progress(lang) })
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,6 +6,12 @@ class ApplicationSetting < ApplicationRecord
|
|||
include TokenAuthenticatable
|
||||
include ChronicDurationAttribute
|
||||
|
||||
# Only remove this >= %12.6 and >= 2019-12-01
|
||||
self.ignored_columns += %i[
|
||||
pendo_enabled
|
||||
pendo_url
|
||||
]
|
||||
|
||||
add_authentication_token_field :runners_registration_token, encrypted: -> { Feature.enabled?(:application_settings_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
|
||||
add_authentication_token_field :health_check_access_token
|
||||
add_authentication_token_field :static_objects_external_storage_auth_token
|
||||
|
@ -103,11 +109,6 @@ class ApplicationSetting < ApplicationRecord
|
|||
allow_blank: true,
|
||||
if: :snowplow_enabled
|
||||
|
||||
validates :pendo_url,
|
||||
presence: true,
|
||||
public_url: true,
|
||||
if: :pendo_enabled
|
||||
|
||||
validates :max_attachment_size,
|
||||
presence: true,
|
||||
numericality: { only_integer: true, greater_than: 0 }
|
||||
|
|
|
@ -135,8 +135,6 @@ module ApplicationSettingImplementation
|
|||
snowplow_app_id: nil,
|
||||
snowplow_iglu_registry_url: nil,
|
||||
custom_http_clone_url_root: nil,
|
||||
pendo_enabled: false,
|
||||
pendo_url: nil,
|
||||
productivity_analytics_start_date: Time.now
|
||||
}
|
||||
end
|
||||
|
|
|
@ -7,5 +7,5 @@
|
|||
= render_if_exists 'admin/application_settings/slack'
|
||||
= render 'admin/application_settings/third_party_offers'
|
||||
= render 'admin/application_settings/snowplow'
|
||||
= render_if_exists 'admin/application_settings/pendo'
|
||||
= render 'admin/application_settings/eks' if Feature.enabled?(:create_eks_clusters)
|
||||
|
||||
|
|
|
@ -90,4 +90,3 @@
|
|||
= render 'layouts/google_analytics' if extra_config.has_key?('google_analytics_id')
|
||||
= render 'layouts/piwik' if extra_config.has_key?('piwik_url') && extra_config.has_key?('piwik_site_id')
|
||||
= render 'layouts/snowplow'
|
||||
= render_if_exists 'layouts/pendo'
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
= render 'shared/commit_well', commit: commit, ref: ref, project: project
|
||||
|
||||
- if is_project_overview
|
||||
.project-buttons.append-bottom-default{ class: ("js-keep-hidden-on-navigation" if vue_file_list_enabled?) }
|
||||
.project-buttons.append-bottom-default{ class: ("js-show-on-project-root" if vue_file_list_enabled?) }
|
||||
= render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout)
|
||||
|
||||
- if vue_file_list_enabled?
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
- max_project_topic_length = 15
|
||||
- emails_disabled = @project.emails_disabled?
|
||||
|
||||
.project-home-panel{ class: [("empty-project" if empty_repo), ("js-keep-hidden-on-navigation" if vue_file_list_enabled?)] }
|
||||
.project-home-panel{ class: [("empty-project" if empty_repo), ("js-show-on-project-root" if vue_file_list_enabled?)] }
|
||||
.row.append-bottom-8
|
||||
.home-panel-title-row.col-md-12.col-lg-6.d-flex
|
||||
.avatar-container.rect-avatar.s64.home-panel-avatar.append-right-default.float-none
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
- if readme.rich_viewer
|
||||
%article.file-holder.readme-holder{ id: 'readme', class: [("limited-width-container" unless fluid_layout), ("js-hide-on-navigation" if vue_file_list_enabled?)] }
|
||||
%article.file-holder.readme-holder{ id: 'readme', class: [("limited-width-container" unless fluid_layout), ("js-show-on-root" if vue_file_list_enabled?)] }
|
||||
.js-file-title.file-title
|
||||
= blob_icon readme.mode, readme.name
|
||||
= link_to project_blob_path(@project, tree_join(@ref, readme.path)) do
|
||||
|
|
5
changelogs/unreleased/31912-epic-labels.yml
Normal file
5
changelogs/unreleased/31912-epic-labels.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Manage and display labels from epic in the GraphQL API
|
||||
merge_request: 19642
|
||||
author:
|
||||
type: added
|
5
changelogs/unreleased/35534-broken-scroll-to-bottom.yml
Normal file
5
changelogs/unreleased/35534-broken-scroll-to-bottom.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix scroll to bottom with new job log
|
||||
merge_request:
|
||||
author:
|
||||
type: fixed
|
5
changelogs/unreleased/infinite-scroll.yml
Normal file
5
changelogs/unreleased/infinite-scroll.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add Infinite scroll to Add Projects modal in the operations dashboard
|
||||
merge_request: 17842
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update help text of "Tag name" field on Edit Release page
|
||||
merge_request: 19321
|
||||
author:
|
||||
type: changed
|
14
db/migrate/20191030135044_create_plan_limits.rb
Normal file
14
db/migrate/20191030135044_create_plan_limits.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreatePlanLimits < ActiveRecord::Migration[5.2]
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
create_table :plan_limits, id: false do |t|
|
||||
t.references :plan, foreign_key: { on_delete: :cascade }, null: false, index: { unique: true }
|
||||
t.integer :ci_active_pipelines, null: false, default: 0
|
||||
t.integer :ci_pipeline_size, null: false, default: 0
|
||||
t.integer :ci_active_jobs, null: false, default: 0
|
||||
end
|
||||
end
|
||||
end
|
17
db/migrate/20191030152934_move_limits_from_plans.rb
Normal file
17
db/migrate/20191030152934_move_limits_from_plans.rb
Normal file
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class MoveLimitsFromPlans < ActiveRecord::Migration[5.2]
|
||||
DOWNTIME = false
|
||||
|
||||
def up
|
||||
execute <<~SQL
|
||||
INSERT INTO plan_limits (plan_id, ci_active_pipelines, ci_pipeline_size, ci_active_jobs)
|
||||
SELECT id, COALESCE(active_pipelines_limit, 0), COALESCE(pipeline_size_limit, 0), COALESCE(active_jobs_limit, 0)
|
||||
FROM plans
|
||||
SQL
|
||||
end
|
||||
|
||||
def down
|
||||
execute 'DELETE FROM plan_limits'
|
||||
end
|
||||
end
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemovePendoFromApplicationSettings < ActiveRecord::Migration[5.2]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
def up
|
||||
remove_column :application_settings, :pendo_enabled
|
||||
remove_column :application_settings, :pendo_url
|
||||
end
|
||||
|
||||
def down
|
||||
add_column_with_default :application_settings, :pendo_enabled, :boolean, default: false, allow_null: false
|
||||
add_column :application_settings, :pendo_url, :string, limit: 255
|
||||
end
|
||||
end
|
17
db/post_migrate/20191031112603_remove_limits_from_plans.rb
Normal file
17
db/post_migrate/20191031112603_remove_limits_from_plans.rb
Normal file
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveLimitsFromPlans < ActiveRecord::Migration[5.2]
|
||||
DOWNTIME = false
|
||||
|
||||
def up
|
||||
remove_column :plans, :active_pipelines_limit
|
||||
remove_column :plans, :pipeline_size_limit
|
||||
remove_column :plans, :active_jobs_limit
|
||||
end
|
||||
|
||||
def down
|
||||
add_column :plans, :active_pipelines_limit, :integer
|
||||
add_column :plans, :pipeline_size_limit, :integer
|
||||
add_column :plans, :active_jobs_limit, :integer
|
||||
end
|
||||
end
|
14
db/schema.rb
14
db/schema.rb
|
@ -342,8 +342,6 @@ ActiveRecord::Schema.define(version: 2019_11_12_115317) do
|
|||
t.integer "push_event_hooks_limit", default: 3, null: false
|
||||
t.integer "push_event_activities_limit", default: 3, null: false
|
||||
t.string "custom_http_clone_url_root", limit: 511
|
||||
t.boolean "pendo_enabled", default: false, null: false
|
||||
t.string "pendo_url", limit: 255
|
||||
t.integer "deletion_adjourned_period", default: 7, null: false
|
||||
t.date "license_trial_ends_on"
|
||||
t.boolean "eks_integration_enabled", default: false, null: false
|
||||
|
@ -2802,14 +2800,19 @@ ActiveRecord::Schema.define(version: 2019_11_12_115317) do
|
|||
t.index ["user_id"], name: "index_personal_access_tokens_on_user_id"
|
||||
end
|
||||
|
||||
create_table "plan_limits", force: :cascade do |t|
|
||||
t.bigint "plan_id", null: false
|
||||
t.integer "ci_active_pipelines", default: 0, null: false
|
||||
t.integer "ci_pipeline_size", default: 0, null: false
|
||||
t.integer "ci_active_jobs", default: 0, null: false
|
||||
t.index ["plan_id"], name: "index_plan_limits_on_plan_id", unique: true
|
||||
end
|
||||
|
||||
create_table "plans", id: :serial, force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.string "name"
|
||||
t.string "title"
|
||||
t.integer "active_pipelines_limit"
|
||||
t.integer "pipeline_size_limit"
|
||||
t.integer "active_jobs_limit", default: 0
|
||||
t.index ["name"], name: "index_plans_on_name"
|
||||
end
|
||||
|
||||
|
@ -4389,6 +4392,7 @@ ActiveRecord::Schema.define(version: 2019_11_12_115317) do
|
|||
add_foreign_key "path_locks", "projects", name: "fk_5265c98f24", on_delete: :cascade
|
||||
add_foreign_key "path_locks", "users"
|
||||
add_foreign_key "personal_access_tokens", "users"
|
||||
add_foreign_key "plan_limits", "plans", on_delete: :cascade
|
||||
add_foreign_key "pool_repositories", "projects", column: "source_project_id", on_delete: :nullify
|
||||
add_foreign_key "pool_repositories", "shards", on_delete: :restrict
|
||||
add_foreign_key "project_alerting_settings", "projects", on_delete: :cascade
|
||||
|
|
|
@ -216,6 +216,11 @@ type CreateDiffNotePayload {
|
|||
Autogenerated input type of CreateEpic
|
||||
"""
|
||||
input CreateEpicInput {
|
||||
"""
|
||||
The IDs of labels to be added to the epic.
|
||||
"""
|
||||
addLabelIds: [ID!]
|
||||
|
||||
"""
|
||||
A unique identifier for the client performing the mutation.
|
||||
"""
|
||||
|
@ -241,6 +246,11 @@ input CreateEpicInput {
|
|||
"""
|
||||
groupPath: ID!
|
||||
|
||||
"""
|
||||
The IDs of labels to be removed from the epic.
|
||||
"""
|
||||
removeLabelIds: [ID!]
|
||||
|
||||
"""
|
||||
The start date of the epic
|
||||
"""
|
||||
|
@ -1171,6 +1181,31 @@ type Epic implements Noteable {
|
|||
last: Int
|
||||
): EpicIssueConnection
|
||||
|
||||
"""
|
||||
Labels assigned to the epic
|
||||
"""
|
||||
labels(
|
||||
"""
|
||||
Returns the elements in the list that come after the specified cursor.
|
||||
"""
|
||||
after: String
|
||||
|
||||
"""
|
||||
Returns the elements in the list that come before the specified cursor.
|
||||
"""
|
||||
before: String
|
||||
|
||||
"""
|
||||
Returns the first _n_ elements from the list.
|
||||
"""
|
||||
first: Int
|
||||
|
||||
"""
|
||||
Returns the last _n_ elements from the list.
|
||||
"""
|
||||
last: Int
|
||||
): LabelConnection
|
||||
|
||||
"""
|
||||
All notes on this noteable
|
||||
"""
|
||||
|
@ -5061,6 +5096,11 @@ type TreeEntryEdge {
|
|||
Autogenerated input type of UpdateEpic
|
||||
"""
|
||||
input UpdateEpicInput {
|
||||
"""
|
||||
The IDs of labels to be added to the epic.
|
||||
"""
|
||||
addLabelIds: [ID!]
|
||||
|
||||
"""
|
||||
A unique identifier for the client performing the mutation.
|
||||
"""
|
||||
|
@ -5091,6 +5131,11 @@ input UpdateEpicInput {
|
|||
"""
|
||||
iid: String!
|
||||
|
||||
"""
|
||||
The IDs of labels to be removed from the epic.
|
||||
"""
|
||||
removeLabelIds: [ID!]
|
||||
|
||||
"""
|
||||
The start date of the epic
|
||||
"""
|
||||
|
|
|
@ -3788,6 +3788,59 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "labels",
|
||||
"description": "Labels assigned to the epic",
|
||||
"args": [
|
||||
{
|
||||
"name": "after",
|
||||
"description": "Returns the elements in the list that come after the specified cursor.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "before",
|
||||
"description": "Returns the elements in the list that come before the specified cursor.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "first",
|
||||
"description": "Returns the first _n_ elements from the list.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "last",
|
||||
"description": "Returns the last _n_ elements from the list.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "LabelConnection",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "notes",
|
||||
"description": "All notes on this noteable",
|
||||
|
@ -6249,6 +6302,213 @@
|
|||
],
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "LabelConnection",
|
||||
"description": "The connection type for Label.",
|
||||
"fields": [
|
||||
{
|
||||
"name": "edges",
|
||||
"description": "A list of edges.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "LabelEdge",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "nodes",
|
||||
"description": "A list of nodes.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "Label",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "pageInfo",
|
||||
"description": "Information to aid in pagination.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "PageInfo",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [
|
||||
|
||||
],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "LabelEdge",
|
||||
"description": "An edge in a connection.",
|
||||
"fields": [
|
||||
{
|
||||
"name": "cursor",
|
||||
"description": "A cursor for use in pagination.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "node",
|
||||
"description": "The item at the end of the edge.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "Label",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [
|
||||
|
||||
],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "Label",
|
||||
"description": null,
|
||||
"fields": [
|
||||
{
|
||||
"name": "color",
|
||||
"description": "Background color of the label",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"description": "Description of the label (markdown rendered as HTML for caching)",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "descriptionHtml",
|
||||
"description": "The GitLab Flavored Markdown rendering of `description`",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "textColor",
|
||||
"description": "Text color of the label",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"description": "Content of the label",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [
|
||||
|
||||
],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "UserConnection",
|
||||
|
@ -7482,213 +7742,6 @@
|
|||
],
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "LabelConnection",
|
||||
"description": "The connection type for Label.",
|
||||
"fields": [
|
||||
{
|
||||
"name": "edges",
|
||||
"description": "A list of edges.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "LabelEdge",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "nodes",
|
||||
"description": "A list of nodes.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "Label",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "pageInfo",
|
||||
"description": "Information to aid in pagination.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "PageInfo",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [
|
||||
|
||||
],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "LabelEdge",
|
||||
"description": "An edge in a connection.",
|
||||
"fields": [
|
||||
{
|
||||
"name": "cursor",
|
||||
"description": "A cursor for use in pagination.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "node",
|
||||
"description": "The item at the end of the edge.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "Label",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [
|
||||
|
||||
],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "Label",
|
||||
"description": null,
|
||||
"fields": [
|
||||
{
|
||||
"name": "color",
|
||||
"description": "Background color of the label",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"description": "Description of the label (markdown rendered as HTML for caching)",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "descriptionHtml",
|
||||
"description": "The GitLab Flavored Markdown rendering of `description`",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "textColor",
|
||||
"description": "Text color of the label",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"description": "Content of the label",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [
|
||||
|
||||
],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "Milestone",
|
||||
|
@ -17012,6 +17065,42 @@
|
|||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "addLabelIds",
|
||||
"description": "The IDs of labels to be added to the epic.",
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "removeLabelIds",
|
||||
"description": "The IDs of labels to be removed from the epic.",
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "iid",
|
||||
"description": "The iid of the epic to mutate",
|
||||
|
@ -17221,6 +17310,42 @@
|
|||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "addLabelIds",
|
||||
"description": "The IDs of labels to be added to the epic.",
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "removeLabelIds",
|
||||
"description": "The IDs of labels to be removed from the epic.",
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "clientMutationId",
|
||||
"description": "A unique identifier for the client performing the mutation.",
|
||||
|
|
|
@ -321,8 +321,6 @@ are listed in the descriptions of the relevant settings.
|
|||
| `snowplow_enabled` | boolean | no | Enable snowplow tracking. |
|
||||
| `snowplow_app_id` | string | no | The Snowplow site name / application id. (e.g. `gitlab`) |
|
||||
| `snowplow_iglu_registry_url` | string | no | The Snowplow base Iglu Schema Registry URL to use for custom context and self describing events'|
|
||||
| `pendo_url` | string | required by: `pendo_enabled` | The Pendo endpoint url with js snippet. (e.g. `https://cdn.pendo.io/agent/static/your-api-key/pendo.js`) |
|
||||
| `pendo_enabled` | boolean | no | Enable pendo tracking. |
|
||||
| `terminal_max_session_time` | integer | no | Maximum time for web terminal websocket connection (in seconds). Set to `0` for unlimited time. |
|
||||
| `terms` | text | required by: `enforce_terms` | (**Required by:** `enforce_terms`) Markdown content for the ToS. |
|
||||
| `throttle_authenticated_api_enabled` | boolean | no | (**If enabled, requires:** `throttle_authenticated_api_period_in_seconds` and `throttle_authenticated_api_requests_per_period`) Enable authenticated API request rate limit. Helps reduce request volume (e.g. from crawlers or abusive bots). |
|
||||
|
|
|
@ -68,7 +68,3 @@ Once enabled, tracking events can be inspected locally by either:
|
|||
|
||||
- Looking at the network panel of the browser's development tools
|
||||
- Using the [Snowplow Chrome Extension](https://chrome.google.com/webstore/detail/snowplow-inspector/maplkdomeamdlngconidoefjpogkmljm).
|
||||
|
||||
## Additional libraries
|
||||
|
||||
Session tracking is handled by [Pendo](https://www.pendo.io/), which is a purely client library and is a relatively minor development concern but is worth including in this documentation.
|
||||
|
|
BIN
doc/user/project/pages/img/new_project_for_pages_v12_5.png
Normal file
BIN
doc/user/project/pages/img/new_project_for_pages_v12_5.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 70 KiB |
BIN
doc/user/project/pages/img/pages_workflow_v12_5.png
Normal file
BIN
doc/user/project/pages/img/pages_workflow_v12_5.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
|
@ -19,38 +19,7 @@ You can use it either for personal or business websites, such as
|
|||
portfolios, documentation, manifestos, and business presentations.
|
||||
You can also attribute any license to your content.
|
||||
|
||||
<table class="borderless-table center fixed-table">
|
||||
<tr>
|
||||
<td style="width: 22%"><img src="img/icons/cogs.png" alt="SSGs" class="image-noshadow half-width"></td>
|
||||
<td style="width: 4%">
|
||||
<strong>
|
||||
<i class="fa fa-angle-double-right" aria-hidden="true"></i>
|
||||
</strong>
|
||||
</td>
|
||||
<td style="width: 22%"><img src="img/icons/monitor.png" alt="Websites" class="image-noshadow half-width"></td>
|
||||
<td style="width: 4%">
|
||||
<strong>
|
||||
<i class="fa fa-angle-double-right" aria-hidden="true"></i>
|
||||
</strong>
|
||||
</td>
|
||||
<td style="width: 22%"><img src="img/icons/free.png" alt="Pages is free" class="image-noshadow half-width"></td>
|
||||
<td style="width: 4%">
|
||||
<strong>
|
||||
<i class="fa fa-angle-double-right" aria-hidden="true"></i>
|
||||
</strong>
|
||||
</td>
|
||||
<td style="width: 22%"><img src="img/icons/lock.png" alt="Secure your website" class="image-noshadow half-width"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><em>Use any static website generator or plain HTML</em></td>
|
||||
<td></td>
|
||||
<td><em>Create websites for your projects, groups, or user account</em></td>
|
||||
<td></td>
|
||||
<td><em>Host on GitLab.com for free, or on your own GitLab instance</em></td>
|
||||
<td></td>
|
||||
<td><em>Connect your custom domain(s) and TLS certificates</em></td>
|
||||
</tr>
|
||||
</table>
|
||||
<img src="img/pages_workflow_v12_5.png" alt="Pages websites workflow" class="image-noshadow">
|
||||
|
||||
Pages is available for free for all GitLab.com users as well as for self-managed
|
||||
instances (GitLab Core, Starter, Premium, and Ultimate).
|
||||
|
@ -95,6 +64,8 @@ To get started with GitLab Pages, you can either:
|
|||
- [Copy an existing sample](getting_started/fork_sample_project.md).
|
||||
- [Create a website from scratch or deploy an existing one](getting_started/new_or_existing_website.md).
|
||||
|
||||
<img src="img/new_project_for_pages_v12_5.png" alt="New projects for GitLab Pages" class="image-noshadow">
|
||||
|
||||
Optional features:
|
||||
|
||||
- Use a [custom domain or subdomain](custom_domains_ssl_tls_certification/index.md#set-up-pages-with-a-custom-domain).
|
||||
|
|
|
@ -147,10 +147,6 @@ module API
|
|||
optional :snowplow_cookie_domain, type: String, desc: 'The Snowplow cookie domain'
|
||||
optional :snowplow_app_id, type: String, desc: 'The Snowplow site name / application id'
|
||||
end
|
||||
optional :pendo_enabled, type: Grape::API::Boolean, desc: 'Enable Pendo tracking'
|
||||
given pendo_enabled: ->(val) { val } do
|
||||
requires :pendo_url, type: String, desc: 'The Pendo url endpoint'
|
||||
end
|
||||
|
||||
ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type|
|
||||
optional :"#{type}_key_restriction",
|
||||
|
|
|
@ -3016,6 +3016,9 @@ msgstr ""
|
|||
msgid "Changes won't take place until the index is %{link_start}recreated%{link_end}."
|
||||
msgstr ""
|
||||
|
||||
msgid "Changing a Release tag is only supported via Releases API. %{linkStart}More information%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Changing group path can have unintended side effects."
|
||||
msgstr ""
|
||||
|
||||
|
@ -3142,9 +3145,6 @@ msgstr ""
|
|||
msgid "Choose a type..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Choose an existing tag, or create a new one"
|
||||
msgstr ""
|
||||
|
||||
msgid "Choose any color."
|
||||
msgstr ""
|
||||
|
||||
|
@ -6229,9 +6229,6 @@ msgstr ""
|
|||
msgid "Enable or disable version check and usage ping."
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable pendo tracking"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable protected paths rate limit"
|
||||
msgstr ""
|
||||
|
||||
|
@ -10660,10 +10657,10 @@ msgstr ""
|
|||
msgid "MergeRequests|started a thread on %{linkStart}the diff%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "MergeRequests|started a thread on an outdated change in commit %{linkStart}%{commitId}%{linkEnd}"
|
||||
msgid "MergeRequests|started a thread on an outdated change in commit %{linkStart}%{commitDisplay}%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "MergeRequests|started a thread on commit %{linkStart}%{commitId}%{linkEnd}"
|
||||
msgid "MergeRequests|started a thread on commit %{linkStart}%{commitDisplay}%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "MergeRequest| %{paragraphStart}changed the description %{descriptionChangedTimes} times %{timeDifferenceMinutes}%{paragraphEnd}"
|
||||
|
@ -11986,12 +11983,6 @@ msgstr ""
|
|||
msgid "Pending"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pendo"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pendo endpoint"
|
||||
msgstr ""
|
||||
|
||||
msgid "People without permission will never get a notification and won't be able to comment."
|
||||
msgstr ""
|
||||
|
||||
|
|
141
spec/frontend/notes/components/diff_discussion_header_spec.js
Normal file
141
spec/frontend/notes/components/diff_discussion_header_spec.js
Normal file
|
@ -0,0 +1,141 @@
|
|||
import { mount, createLocalVue } from '@vue/test-utils';
|
||||
|
||||
import createStore from '~/notes/stores';
|
||||
import diffDiscussionHeader from '~/notes/components/diff_discussion_header.vue';
|
||||
|
||||
import { discussionMock } from '../../../javascripts/notes/mock_data';
|
||||
import mockDiffFile from '../../diffs/mock_data/diff_discussions';
|
||||
|
||||
const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json';
|
||||
|
||||
describe('diff_discussion_header component', () => {
|
||||
let store;
|
||||
let wrapper;
|
||||
|
||||
preloadFixtures(discussionWithTwoUnresolvedNotes);
|
||||
|
||||
beforeEach(() => {
|
||||
window.mrTabs = {};
|
||||
store = createStore();
|
||||
|
||||
const localVue = createLocalVue();
|
||||
wrapper = mount(diffDiscussionHeader, {
|
||||
store,
|
||||
propsData: { discussion: discussionMock },
|
||||
localVue,
|
||||
sync: false,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('should render user avatar', () => {
|
||||
const discussion = { ...discussionMock };
|
||||
discussion.diff_file = mockDiffFile;
|
||||
discussion.diff_discussion = true;
|
||||
|
||||
wrapper.setProps({ discussion });
|
||||
|
||||
expect(wrapper.find('.user-avatar-link').exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('action text', () => {
|
||||
const commitId = 'razupaltuff';
|
||||
const truncatedCommitId = commitId.substr(0, 8);
|
||||
let commitElement;
|
||||
|
||||
beforeEach(done => {
|
||||
store.state.diffs = {
|
||||
projectPath: 'something',
|
||||
};
|
||||
|
||||
wrapper.setProps({
|
||||
discussion: {
|
||||
...discussionMock,
|
||||
for_commit: true,
|
||||
commit_id: commitId,
|
||||
diff_discussion: true,
|
||||
diff_file: {
|
||||
...mockDiffFile,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
wrapper.vm
|
||||
.$nextTick()
|
||||
.then(() => {
|
||||
commitElement = wrapper.find('.commit-sha');
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
describe('for diff threads without a commit id', () => {
|
||||
it('should show started a thread on the diff text', done => {
|
||||
Object.assign(wrapper.vm.discussion, {
|
||||
for_commit: false,
|
||||
commit_id: null,
|
||||
});
|
||||
|
||||
wrapper.vm.$nextTick(() => {
|
||||
expect(wrapper.text()).toContain('started a thread on the diff');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show thread on older version text', done => {
|
||||
Object.assign(wrapper.vm.discussion, {
|
||||
for_commit: false,
|
||||
commit_id: null,
|
||||
active: false,
|
||||
});
|
||||
|
||||
wrapper.vm.$nextTick(() => {
|
||||
expect(wrapper.text()).toContain('started a thread on an old version of the diff');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('for commit threads', () => {
|
||||
it('should display a monospace started a thread on commit', () => {
|
||||
expect(wrapper.text()).toContain(`started a thread on commit ${truncatedCommitId}`);
|
||||
expect(commitElement.exists()).toBe(true);
|
||||
expect(commitElement.text()).toContain(truncatedCommitId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('for diff thread with a commit id', () => {
|
||||
it('should display started thread on commit header', done => {
|
||||
wrapper.vm.discussion.for_commit = false;
|
||||
|
||||
wrapper.vm.$nextTick(() => {
|
||||
expect(wrapper.text()).toContain(`started a thread on commit ${truncatedCommitId}`);
|
||||
|
||||
expect(commitElement).not.toBe(null);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display outdated change on commit header', done => {
|
||||
wrapper.vm.discussion.for_commit = false;
|
||||
wrapper.vm.discussion.active = false;
|
||||
|
||||
wrapper.vm.$nextTick(() => {
|
||||
expect(wrapper.text()).toContain(
|
||||
`started a thread on an outdated change in commit ${truncatedCommitId}`,
|
||||
);
|
||||
|
||||
expect(commitElement).not.toBe(null);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -8,15 +8,17 @@ describe('Release detail component', () => {
|
|||
let wrapper;
|
||||
let releaseClone;
|
||||
let actions;
|
||||
let state;
|
||||
|
||||
beforeEach(() => {
|
||||
gon.api_version = 'v4';
|
||||
|
||||
releaseClone = JSON.parse(JSON.stringify(convertObjectPropsToCamelCase(release)));
|
||||
|
||||
const state = {
|
||||
state = {
|
||||
release: releaseClone,
|
||||
markdownDocsPath: 'path/to/markdown/docs',
|
||||
updateReleaseApiDocsPath: 'path/to/update/release/api/docs',
|
||||
};
|
||||
|
||||
actions = {
|
||||
|
@ -46,6 +48,21 @@ describe('Release detail component', () => {
|
|||
expect(wrapper.find('#git-ref').element.value).toBe(releaseClone.tagName);
|
||||
});
|
||||
|
||||
it('renders the correct help text under the "Tag name" field', () => {
|
||||
const helperText = wrapper.find('#tag-name-help');
|
||||
const helperTextLink = helperText.find('a');
|
||||
const helperTextLinkAttrs = helperTextLink.attributes();
|
||||
|
||||
expect(helperText.text()).toBe(
|
||||
'Changing a Release tag is only supported via Releases API. More information',
|
||||
);
|
||||
expect(helperTextLink.text()).toBe('More information');
|
||||
expect(helperTextLinkAttrs.href).toBe(state.updateReleaseApiDocsPath);
|
||||
expect(helperTextLinkAttrs.rel).toContain('noopener');
|
||||
expect(helperTextLinkAttrs.rel).toContain('noreferrer');
|
||||
expect(helperTextLinkAttrs.target).toBe('_blank');
|
||||
});
|
||||
|
||||
it('renders the correct release title in the "Release title" field', () => {
|
||||
expect(wrapper.find('#release-title').element.value).toBe(releaseClone.name);
|
||||
});
|
||||
|
|
42
spec/frontend/repository/pages/index_spec.js
Normal file
42
spec/frontend/repository/pages/index_spec.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import IndexPage from '~/repository/pages/index.vue';
|
||||
import TreePage from '~/repository/pages/tree.vue';
|
||||
import { updateElementsVisibility } from '~/repository/utils/dom';
|
||||
|
||||
jest.mock('~/repository/utils/dom');
|
||||
|
||||
describe('Repository index page component', () => {
|
||||
let wrapper;
|
||||
|
||||
function factory() {
|
||||
wrapper = shallowMount(IndexPage);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
|
||||
updateElementsVisibility.mockClear();
|
||||
});
|
||||
|
||||
it('calls updateElementsVisibility on mounted', () => {
|
||||
factory();
|
||||
|
||||
expect(updateElementsVisibility).toHaveBeenCalledWith('.js-show-on-project-root', true);
|
||||
});
|
||||
|
||||
it('calls updateElementsVisibility after destroy', () => {
|
||||
factory();
|
||||
wrapper.destroy();
|
||||
|
||||
expect(updateElementsVisibility.mock.calls.pop()).toEqual(['.js-show-on-project-root', false]);
|
||||
});
|
||||
|
||||
it('renders TreePage', () => {
|
||||
factory();
|
||||
|
||||
const child = wrapper.find(TreePage);
|
||||
|
||||
expect(child.exists()).toBe(true);
|
||||
expect(child.props()).toEqual({ path: '/' });
|
||||
});
|
||||
});
|
60
spec/frontend/repository/pages/tree_spec.js
Normal file
60
spec/frontend/repository/pages/tree_spec.js
Normal file
|
@ -0,0 +1,60 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import TreePage from '~/repository/pages/tree.vue';
|
||||
import { updateElementsVisibility } from '~/repository/utils/dom';
|
||||
|
||||
jest.mock('~/repository/utils/dom');
|
||||
|
||||
describe('Repository tree page component', () => {
|
||||
let wrapper;
|
||||
|
||||
function factory(path) {
|
||||
wrapper = shallowMount(TreePage, { propsData: { path } });
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
|
||||
updateElementsVisibility.mockClear();
|
||||
});
|
||||
|
||||
describe('when root path', () => {
|
||||
beforeEach(() => {
|
||||
factory('/');
|
||||
});
|
||||
|
||||
it('shows root elements', () => {
|
||||
expect(updateElementsVisibility.mock.calls).toEqual([
|
||||
['.js-show-on-root', true],
|
||||
['.js-hide-on-root', false],
|
||||
]);
|
||||
});
|
||||
|
||||
describe('when changed', () => {
|
||||
beforeEach(() => {
|
||||
updateElementsVisibility.mockClear();
|
||||
|
||||
wrapper.setProps({ path: '/test' });
|
||||
});
|
||||
|
||||
it('hides root elements', () => {
|
||||
expect(updateElementsVisibility.mock.calls).toEqual([
|
||||
['.js-show-on-root', false],
|
||||
['.js-hide-on-root', true],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when non-root path', () => {
|
||||
beforeEach(() => {
|
||||
factory('/test');
|
||||
});
|
||||
|
||||
it('hides root elements', () => {
|
||||
expect(updateElementsVisibility.mock.calls).toEqual([
|
||||
['.js-show-on-root', false],
|
||||
['.js-hide-on-root', true],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
20
spec/frontend/repository/utils/dom_spec.js
Normal file
20
spec/frontend/repository/utils/dom_spec.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { setHTMLFixture } from '../../helpers/fixtures';
|
||||
import { updateElementsVisibility } from '~/repository/utils/dom';
|
||||
|
||||
describe('updateElementsVisibility', () => {
|
||||
it('adds hidden class', () => {
|
||||
setHTMLFixture('<div class="js-test"></div>');
|
||||
|
||||
updateElementsVisibility('.js-test', false);
|
||||
|
||||
expect(document.querySelector('.js-test').classList).toContain('hidden');
|
||||
});
|
||||
|
||||
it('removes hidden class', () => {
|
||||
setHTMLFixture('<div class="hidden js-test"></div>');
|
||||
|
||||
updateElementsVisibility('.js-test', true);
|
||||
|
||||
expect(document.querySelector('.js-test').classList).not.toContain('hidden');
|
||||
});
|
||||
});
|
|
@ -8,8 +8,8 @@ describe('setTitle', () => {
|
|||
${'app/assets'} | ${'app/assets'}
|
||||
${'app/assets/javascripts'} | ${'app/assets/javascripts'}
|
||||
`('sets document title as $title for $path', ({ path, title }) => {
|
||||
setTitle(path, 'master', 'GitLab');
|
||||
setTitle(path, 'master', 'GitLab Org / GitLab');
|
||||
|
||||
expect(document.title).toEqual(`${title} · master · GitLab`);
|
||||
expect(document.title).toEqual(`${title} · master · GitLab Org / GitLab · GitLab`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -39,7 +39,6 @@ describe ApplicationSettingsHelper do
|
|||
|
||||
context 'with tracking parameters' do
|
||||
it { expect(visible_attributes).to include(*%i(snowplow_collector_hostname snowplow_cookie_domain snowplow_enabled snowplow_app_id)) }
|
||||
it { expect(visible_attributes).to include(*%i(pendo_enabled pendo_url)) }
|
||||
end
|
||||
|
||||
describe '.integration_expanded?' do
|
||||
|
|
|
@ -17,9 +17,11 @@ describe ReleasesHelper do
|
|||
|
||||
context 'url helpers' do
|
||||
let(:project) { build(:project, namespace: create(:group)) }
|
||||
let(:release) { create(:release, project: project) }
|
||||
|
||||
before do
|
||||
helper.instance_variable_set(:@project, project)
|
||||
helper.instance_variable_set(:@release, release)
|
||||
end
|
||||
|
||||
describe '#data_for_releases_page' do
|
||||
|
@ -28,5 +30,17 @@ describe ReleasesHelper do
|
|||
expect(helper.data_for_releases_page.keys).to eq(keys)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#data_for_edit_release_page' do
|
||||
it 'has the needed data to display the "edit release" page' do
|
||||
keys = %i(project_id
|
||||
tag_name
|
||||
markdown_preview_path
|
||||
markdown_docs_path
|
||||
releases_page_path
|
||||
update_release_api_docs_path)
|
||||
expect(helper.data_for_edit_release_page.keys).to eq(keys)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -247,7 +247,7 @@ describe('Frequent Items App Component', () => {
|
|||
|
||||
.then(() => {
|
||||
expect(vm.$el.querySelectorAll('.frequent-items-list-container li').length).toBe(
|
||||
mockSearchedProjects.length,
|
||||
mockSearchedProjects.data.length,
|
||||
);
|
||||
})
|
||||
.then(done)
|
||||
|
|
|
@ -68,7 +68,7 @@ export const mockFrequentGroups = [
|
|||
},
|
||||
];
|
||||
|
||||
export const mockSearchedGroups = [mockRawGroup];
|
||||
export const mockSearchedGroups = { data: [mockRawGroup] };
|
||||
export const mockProcessedSearchedGroups = [mockGroup];
|
||||
|
||||
export const mockProject = {
|
||||
|
@ -135,7 +135,7 @@ export const mockFrequentProjects = [
|
|||
},
|
||||
];
|
||||
|
||||
export const mockSearchedProjects = [mockRawProject];
|
||||
export const mockSearchedProjects = { data: [mockRawProject] };
|
||||
export const mockProcessedSearchedProjects = [mockProject];
|
||||
|
||||
export const unsortedFrequentItems = [
|
||||
|
|
|
@ -169,7 +169,7 @@ describe('Frequent Items Dropdown Store Actions', () => {
|
|||
});
|
||||
|
||||
it('should dispatch `receiveSearchedItemsSuccess`', done => {
|
||||
mock.onGet(/\/api\/v4\/projects.json(.*)$/).replyOnce(200, mockSearchedProjects);
|
||||
mock.onGet(/\/api\/v4\/projects.json(.*)$/).replyOnce(200, mockSearchedProjects, {});
|
||||
|
||||
testAction(
|
||||
actions.fetchSearchedItems,
|
||||
|
@ -178,7 +178,10 @@ describe('Frequent Items Dropdown Store Actions', () => {
|
|||
[],
|
||||
[
|
||||
{ type: 'requestSearchedItems' },
|
||||
{ type: 'receiveSearchedItemsSuccess', payload: mockSearchedProjects },
|
||||
{
|
||||
type: 'receiveSearchedItemsSuccess',
|
||||
payload: { data: mockSearchedProjects, headers: {} },
|
||||
},
|
||||
],
|
||||
done,
|
||||
);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import { mount, createLocalVue } from '@vue/test-utils';
|
||||
import createStore from '~/notes/stores';
|
||||
import noteableDiscussion from '~/notes/components/noteable_discussion.vue';
|
||||
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
|
||||
|
@ -23,7 +23,7 @@ describe('noteable_discussion component', () => {
|
|||
store.dispatch('setNotesData', notesDataMock);
|
||||
|
||||
const localVue = createLocalVue();
|
||||
wrapper = shallowMount(noteableDiscussion, {
|
||||
wrapper = mount(noteableDiscussion, {
|
||||
store,
|
||||
propsData: { discussion: discussionMock },
|
||||
localVue,
|
||||
|
@ -35,16 +35,6 @@ describe('noteable_discussion component', () => {
|
|||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('should render user avatar', () => {
|
||||
const discussion = { ...discussionMock };
|
||||
discussion.diff_file = mockDiffFile;
|
||||
discussion.diff_discussion = true;
|
||||
|
||||
wrapper.setProps({ discussion, renderDiffFile: true });
|
||||
|
||||
expect(wrapper.find('.user-avatar-link').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should not render thread header for non diff threads', () => {
|
||||
expect(wrapper.find('.discussion-header').exists()).toBe(false);
|
||||
});
|
||||
|
@ -134,105 +124,6 @@ describe('noteable_discussion component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('action text', () => {
|
||||
const commitId = 'razupaltuff';
|
||||
const truncatedCommitId = commitId.substr(0, 8);
|
||||
let commitElement;
|
||||
|
||||
beforeEach(done => {
|
||||
store.state.diffs = {
|
||||
projectPath: 'something',
|
||||
};
|
||||
|
||||
wrapper.setProps({
|
||||
discussion: {
|
||||
...discussionMock,
|
||||
for_commit: true,
|
||||
commit_id: commitId,
|
||||
diff_discussion: true,
|
||||
diff_file: {
|
||||
...mockDiffFile,
|
||||
},
|
||||
},
|
||||
renderDiffFile: true,
|
||||
});
|
||||
|
||||
wrapper.vm
|
||||
.$nextTick()
|
||||
.then(() => {
|
||||
commitElement = wrapper.find('.commit-sha');
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
describe('for commit threads', () => {
|
||||
it('should display a monospace started a thread on commit', () => {
|
||||
expect(wrapper.text()).toContain(`started a thread on commit ${truncatedCommitId}`);
|
||||
expect(commitElement.exists()).toBe(true);
|
||||
expect(commitElement.text()).toContain(truncatedCommitId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('for diff thread with a commit id', () => {
|
||||
it('should display started thread on commit header', done => {
|
||||
wrapper.vm.discussion.for_commit = false;
|
||||
|
||||
wrapper.vm.$nextTick(() => {
|
||||
expect(wrapper.text()).toContain(`started a thread on commit ${truncatedCommitId}`);
|
||||
|
||||
expect(commitElement).not.toBe(null);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display outdated change on commit header', done => {
|
||||
wrapper.vm.discussion.for_commit = false;
|
||||
wrapper.vm.discussion.active = false;
|
||||
|
||||
wrapper.vm.$nextTick(() => {
|
||||
expect(wrapper.text()).toContain(
|
||||
`started a thread on an outdated change in commit ${truncatedCommitId}`,
|
||||
);
|
||||
|
||||
expect(commitElement).not.toBe(null);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('for diff threads without a commit id', () => {
|
||||
it('should show started a thread on the diff text', done => {
|
||||
Object.assign(wrapper.vm.discussion, {
|
||||
for_commit: false,
|
||||
commit_id: null,
|
||||
});
|
||||
|
||||
wrapper.vm.$nextTick(() => {
|
||||
expect(wrapper.text()).toContain('started a thread on the diff');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show thread on older version text', done => {
|
||||
Object.assign(wrapper.vm.discussion, {
|
||||
for_commit: false,
|
||||
commit_id: null,
|
||||
active: false,
|
||||
});
|
||||
|
||||
wrapper.vm.$nextTick(() => {
|
||||
expect(wrapper.text()).toContain('started a thread on an old version of the diff');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('for resolved thread', () => {
|
||||
beforeEach(() => {
|
||||
const discussion = getJSONFixture(discussionWithTwoUnresolvedNotes)[0];
|
||||
|
@ -262,6 +153,7 @@ describe('noteable_discussion component', () => {
|
|||
}));
|
||||
|
||||
wrapper.setProps({ discussion });
|
||||
|
||||
wrapper.vm
|
||||
.$nextTick()
|
||||
.then(done)
|
||||
|
|
|
@ -3,7 +3,7 @@ import _ from 'underscore';
|
|||
import ProjectSelector from '~/vue_shared/components/project_selector/project_selector.vue';
|
||||
import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue';
|
||||
|
||||
import { GlSearchBoxByType } from '@gitlab/ui';
|
||||
import { GlSearchBoxByType, GlInfiniteScroll } from '@gitlab/ui';
|
||||
import { mount, createLocalVue } from '@vue/test-utils';
|
||||
import { trimText } from 'spec/helpers/text_helper';
|
||||
|
||||
|
@ -91,6 +91,13 @@ describe('ProjectSelector component', () => {
|
|||
expect(searchInput.attributes('placeholder')).toBe('Search your projects');
|
||||
});
|
||||
|
||||
it(`triggers a "bottomReached" event when user has scrolled to the bottom of the list`, () => {
|
||||
spyOn(vm, '$emit');
|
||||
wrapper.find(GlInfiniteScroll).vm.$emit('bottomReached');
|
||||
|
||||
expect(vm.$emit).toHaveBeenCalledWith('bottomReached');
|
||||
});
|
||||
|
||||
it(`triggers a "projectClicked" event when a project is clicked`, () => {
|
||||
spyOn(vm, '$emit');
|
||||
wrapper.find(ProjectListItem).vm.$emit('click', _.first(searchResults));
|
||||
|
|
37
spec/migrations/move_limits_from_plans_spec.rb
Normal file
37
spec/migrations/move_limits_from_plans_spec.rb
Normal file
|
@ -0,0 +1,37 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require Rails.root.join('db', 'migrate', '20191030152934_move_limits_from_plans.rb')
|
||||
|
||||
describe MoveLimitsFromPlans, :migration do
|
||||
let(:plans) { table(:plans) }
|
||||
let(:plan_limits) { table(:plan_limits) }
|
||||
|
||||
let!(:early_adopter_plan) { plans.create(name: 'early_adopter', title: 'Early adopter', active_pipelines_limit: 10, pipeline_size_limit: 11, active_jobs_limit: 12) }
|
||||
let!(:gold_plan) { plans.create(name: 'gold', title: 'Gold', active_pipelines_limit: 20, pipeline_size_limit: 21, active_jobs_limit: 22) }
|
||||
let!(:silver_plan) { plans.create(name: 'silver', title: 'Silver', active_pipelines_limit: 30, pipeline_size_limit: 31, active_jobs_limit: 32) }
|
||||
let!(:bronze_plan) { plans.create(name: 'bronze', title: 'Bronze', active_pipelines_limit: 40, pipeline_size_limit: 41, active_jobs_limit: 42) }
|
||||
let!(:free_plan) { plans.create(name: 'free', title: 'Free', active_pipelines_limit: 50, pipeline_size_limit: 51, active_jobs_limit: 52) }
|
||||
let!(:other_plan) { plans.create(name: 'other', title: 'Other', active_pipelines_limit: nil, pipeline_size_limit: nil, active_jobs_limit: 0) }
|
||||
|
||||
describe 'migrate' do
|
||||
it 'populates plan_limits from all the records in plans' do
|
||||
expect { migrate! }.to change { plan_limits.count }.by 6
|
||||
end
|
||||
|
||||
it 'copies plan limits and plan.id into to plan_limits table' do
|
||||
migrate!
|
||||
|
||||
new_data = plan_limits.pluck(:plan_id, :ci_active_pipelines, :ci_pipeline_size, :ci_active_jobs)
|
||||
expected_data = [
|
||||
[early_adopter_plan.id, 10, 11, 12],
|
||||
[gold_plan.id, 20, 21, 22],
|
||||
[silver_plan.id, 30, 31, 32],
|
||||
[bronze_plan.id, 40, 41, 42],
|
||||
[free_plan.id, 50, 51, 52],
|
||||
[other_plan.id, 0, 0, 0]
|
||||
]
|
||||
expect(new_data).to contain_exactly(*expected_data)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -82,22 +82,6 @@ describe ApplicationSetting do
|
|||
it { is_expected.to allow_value(nil).for(:snowplow_iglu_registry_url) }
|
||||
end
|
||||
|
||||
context 'when pendo is enabled' do
|
||||
before do
|
||||
setting.pendo_enabled = true
|
||||
end
|
||||
|
||||
it { is_expected.not_to allow_value(nil).for(:pendo_url) }
|
||||
it { is_expected.to allow_value(http).for(:pendo_url) }
|
||||
it { is_expected.to allow_value(https).for(:pendo_url) }
|
||||
it { is_expected.not_to allow_value(ftp).for(:pendo_url) }
|
||||
it { is_expected.not_to allow_value('http://127.0.0.1').for(:pendo_url) }
|
||||
end
|
||||
|
||||
context 'when pendo is not enabled' do
|
||||
it { is_expected.to allow_value(nil).for(:pendo_url) }
|
||||
end
|
||||
|
||||
context "when user accepted let's encrypt terms of service" do
|
||||
before do
|
||||
setting.update(lets_encrypt_terms_of_service_accepted: true)
|
||||
|
|
|
@ -223,54 +223,6 @@ describe API::Settings, 'Settings' do
|
|||
end
|
||||
end
|
||||
|
||||
context "pendo tracking settings" do
|
||||
let(:settings) do
|
||||
{
|
||||
pendo_url: "https://pendo.example.com",
|
||||
pendo_enabled: true
|
||||
}
|
||||
end
|
||||
|
||||
let(:attribute_names) { settings.keys.map(&:to_s) }
|
||||
|
||||
it "includes the attributes in the API" do
|
||||
get api("/application/settings", admin)
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
attribute_names.each do |attribute|
|
||||
expect(json_response.keys).to include(attribute)
|
||||
end
|
||||
end
|
||||
|
||||
it "allows updating the settings" do
|
||||
put api("/application/settings", admin), params: settings
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
settings.each do |attribute, value|
|
||||
expect(ApplicationSetting.current.public_send(attribute)).to eq(value)
|
||||
end
|
||||
end
|
||||
|
||||
context "missing pendo_url value when pendo_enabled is true" do
|
||||
it "returns a blank parameter error message" do
|
||||
put api("/application/settings", admin), params: { pendo_enabled: true }
|
||||
|
||||
expect(response).to have_gitlab_http_status(400)
|
||||
expect(json_response["error"]).to eq("pendo_url is missing")
|
||||
end
|
||||
|
||||
it "handles validation errors" do
|
||||
put api("/application/settings", admin), params: settings.merge({
|
||||
pendo_url: nil
|
||||
})
|
||||
|
||||
expect(response).to have_gitlab_http_status(400)
|
||||
message = json_response["message"]
|
||||
expect(message["pendo_url"]).to include("can't be blank")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'EKS integration settings' do
|
||||
let(:attribute_names) { settings.keys.map(&:to_s) }
|
||||
let(:sensitive_attributes) { %w(eks_secret_access_key) }
|
||||
|
|
|
@ -606,6 +606,24 @@ describe Issues::UpdateService, :mailer do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when same id is passed as add_label_ids and remove_label_ids' do
|
||||
let(:params) { { add_label_ids: [label.id], remove_label_ids: [label.id] } }
|
||||
|
||||
context 'for a label assigned to an issue' do
|
||||
it 'removes the label' do
|
||||
issue.update(labels: [label])
|
||||
|
||||
expect(result.label_ids).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a label not assigned to an issue' do
|
||||
it 'does not add the label' do
|
||||
expect(result.label_ids).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when duplicate label titles are given' do
|
||||
let(:params) do
|
||||
{ labels: [label3.title, label3.title] }
|
||||
|
|
Loading…
Reference in a new issue