summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2019-11-12 18:06:57 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2019-11-12 18:06:57 +0000
commit6d31b8f052d30b7e55128d17b66bceed8c6065a9 (patch)
treeca428cf6145af7cfaada94378e66bd5e7cc5a429
parent69944ffb68788d190e81ff7e33db5dcb6c903184 (diff)
downloadgitlab-ce-6d31b8f052d30b7e55128d17b66bceed8c6065a9.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/api.js5
-rw-r--r--app/assets/javascripts/frequent_items/store/mutations.js3
-rw-r--r--app/assets/javascripts/jobs/components/log/log.vue26
-rw-r--r--app/assets/javascripts/notes/components/diff_discussion_header.vue133
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue111
-rw-r--r--app/assets/javascripts/releases/detail/components/app.vue46
-rw-r--r--app/assets/javascripts/releases/detail/store/state.js1
-rw-r--r--app/assets/javascripts/repository/index.js14
-rw-r--r--app/assets/javascripts/repository/pages/index.vue18
-rw-r--r--app/assets/javascripts/repository/pages/tree.vue18
-rw-r--r--app/assets/javascripts/repository/utils/dom.js4
-rw-r--r--app/assets/javascripts/repository/utils/title.js8
-rw-r--r--app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue42
-rw-r--r--app/helpers/application_settings_helper.rb4
-rw-r--r--app/helpers/releases_helper.rb3
-rw-r--r--app/helpers/repository_languages_helper.rb2
-rw-r--r--app/models/application_setting.rb11
-rw-r--r--app/models/application_setting_implementation.rb2
-rw-r--r--app/views/admin/application_settings/integrations.html.haml2
-rw-r--r--app/views/layouts/_head.html.haml1
-rw-r--r--app/views/projects/_files.html.haml2
-rw-r--r--app/views/projects/_home_panel.html.haml2
-rw-r--r--app/views/projects/tree/_readme.html.haml2
-rw-r--r--changelogs/unreleased/31912-epic-labels.yml5
-rw-r--r--changelogs/unreleased/35534-broken-scroll-to-bottom.yml5
-rw-r--r--changelogs/unreleased/infinite-scroll.yml5
-rw-r--r--changelogs/unreleased/nfriend-edit-release-ux-cleanup.yml5
-rw-r--r--db/migrate/20191030135044_create_plan_limits.rb14
-rw-r--r--db/migrate/20191030152934_move_limits_from_plans.rb17
-rw-r--r--db/post_migrate/20191030193050_remove_pendo_from_application_settings.rb19
-rw-r--r--db/post_migrate/20191031112603_remove_limits_from_plans.rb17
-rw-r--r--db/schema.rb14
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql45
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json539
-rw-r--r--doc/api/settings.md2
-rw-r--r--doc/development/event_tracking/index.md4
-rw-r--r--doc/user/project/pages/img/new_project_for_pages_v12_5.pngbin0 -> 71618 bytes
-rw-r--r--doc/user/project/pages/img/pages_workflow_v12_5.pngbin0 -> 29541 bytes
-rw-r--r--doc/user/project/pages/index.md35
-rw-r--r--lib/api/settings.rb4
-rw-r--r--locale/gitlab.pot19
-rw-r--r--spec/frontend/notes/components/diff_discussion_header_spec.js141
-rw-r--r--spec/frontend/releases/detail/components/app_spec.js19
-rw-r--r--spec/frontend/repository/pages/index_spec.js42
-rw-r--r--spec/frontend/repository/pages/tree_spec.js60
-rw-r--r--spec/frontend/repository/utils/dom_spec.js20
-rw-r--r--spec/frontend/repository/utils/title_spec.js4
-rw-r--r--spec/helpers/application_settings_helper_spec.rb1
-rw-r--r--spec/helpers/releases_helper_spec.rb14
-rw-r--r--spec/javascripts/frequent_items/components/app_spec.js2
-rw-r--r--spec/javascripts/frequent_items/mock_data.js4
-rw-r--r--spec/javascripts/frequent_items/store/actions_spec.js7
-rw-r--r--spec/javascripts/notes/components/noteable_discussion_spec.js114
-rw-r--r--spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js9
-rw-r--r--spec/migrations/move_limits_from_plans_spec.rb37
-rw-r--r--spec/models/application_setting_spec.rb16
-rw-r--r--spec/requests/api/settings_spec.rb48
-rw-r--r--spec/services/issues/update_service_spec.rb18
58 files changed, 1136 insertions, 629 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 735cbb8e356..aee9990bc0b 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -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 };
});
},
diff --git a/app/assets/javascripts/frequent_items/store/mutations.js b/app/assets/javascripts/frequent_items/store/mutations.js
index 41b660a243f..92ac3a2c94d 100644
--- a/app/assets/javascripts/frequent_items/store/mutations.js
+++ b/app/assets/javascripts/frequent_items/store/mutations.js
@@ -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,
diff --git a/app/assets/javascripts/jobs/components/log/log.vue b/app/assets/javascripts/jobs/components/log/log.vue
index ef126166e8b..03a697d11ed 100644
--- a/app/assets/javascripts/jobs/components/log/log.vue
+++ b/app/assets/javascripts/jobs/components/log/log.vue
@@ -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>
diff --git a/app/assets/javascripts/notes/components/diff_discussion_header.vue b/app/assets/javascripts/notes/components/diff_discussion_header.vue
new file mode 100644
index 00000000000..4c9075912ee
--- /dev/null
+++ b/app/assets/javascripts/notes/components/diff_discussion_header.vue
@@ -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>
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index cb1975a8962..47ec740b63a 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -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"
diff --git a/app/assets/javascripts/releases/detail/components/app.vue b/app/assets/javascripts/releases/detail/components/app.vue
index 54a441de886..073cfcd7694 100644
--- a/app/assets/javascripts/releases/detail/components/app.vue
+++ b/app/assets/javascripts/releases/detail/components/app.vue
@@ -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
diff --git a/app/assets/javascripts/releases/detail/store/state.js b/app/assets/javascripts/releases/detail/store/state.js
index ff98e2bed78..7e3d975f1ae 100644
--- a/app/assets/javascripts/releases/detail/store/state.js
+++ b/app/assets/javascripts/releases/detail/store/state.js
@@ -4,6 +4,7 @@ export default () => ({
releasesPagePath: null,
markdownDocsPath: null,
markdownPreviewPath: null,
+ updateReleaseApiDocsPath: null,
release: null,
diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js
index de7350f0d2f..d826f209815 100644
--- a/app/assets/javascripts/repository/index.js
+++ b/app/assets/javascripts/repository/index.js
@@ -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');
diff --git a/app/assets/javascripts/repository/pages/index.vue b/app/assets/javascripts/repository/pages/index.vue
index 967f4a99281..29786bf4ec8 100644
--- a/app/assets/javascripts/repository/pages/index.vue
+++ b/app/assets/javascripts/repository/pages/index.vue
@@ -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>
diff --git a/app/assets/javascripts/repository/pages/tree.vue b/app/assets/javascripts/repository/pages/tree.vue
index 19300099449..dd4d437f4dd 100644
--- a/app/assets/javascripts/repository/pages/tree.vue
+++ b/app/assets/javascripts/repository/pages/tree.vue
@@ -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>
diff --git a/app/assets/javascripts/repository/utils/dom.js b/app/assets/javascripts/repository/utils/dom.js
new file mode 100644
index 00000000000..963e6fc0bc4
--- /dev/null
+++ b/app/assets/javascripts/repository/utils/dom.js
@@ -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));
+};
diff --git a/app/assets/javascripts/repository/utils/title.js b/app/assets/javascripts/repository/utils/title.js
index 87d54c01200..ff16fbdd420 100644
--- a/app/assets/javascripts/repository/utils/title.js
+++ b/app/assets/javascripts/repository/utils/title.js
@@ -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}`;
};
diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
index 478e44d104c..f984a0a6203 100644
--- a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
+++ b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
@@ -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>
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 120958eb631..8b3d4fbf96a 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -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
diff --git a/app/helpers/releases_helper.rb b/app/helpers/releases_helper.rb
index 68a19152d8f..c4fe40a0875 100644
--- a/app/helpers/releases_helper.rb
+++ b/app/helpers/releases_helper.rb
@@ -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
diff --git a/app/helpers/repository_languages_helper.rb b/app/helpers/repository_languages_helper.rb
index cf7eee7fff3..7834e86adab 100644
--- a/app/helpers/repository_languages_helper.rb
+++ b/app/helpers/repository_languages_helper.rb
@@ -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
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 6a34f293a4a..c037627570a 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -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 }
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 2b2492a793a..77fbe09d4f9 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -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
diff --git a/app/views/admin/application_settings/integrations.html.haml b/app/views/admin/application_settings/integrations.html.haml
index 519d2bf9bbc..3f459e0f491 100644
--- a/app/views/admin/application_settings/integrations.html.haml
+++ b/app/views/admin/application_settings/integrations.html.haml
@@ -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)
+
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 0c4c48447c9..0060d8323b0 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -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'
diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml
index 6681bb4d094..20d4084f428 100644
--- a/app/views/projects/_files.html.haml
+++ b/app/views/projects/_files.html.haml
@@ -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?
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 4783b10cf6d..b7c4114d485 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -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
diff --git a/app/views/projects/tree/_readme.html.haml b/app/views/projects/tree/_readme.html.haml
index 4f6c7e1f9a6..fef019e1b69 100644
--- a/app/views/projects/tree/_readme.html.haml
+++ b/app/views/projects/tree/_readme.html.haml
@@ -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
diff --git a/changelogs/unreleased/31912-epic-labels.yml b/changelogs/unreleased/31912-epic-labels.yml
new file mode 100644
index 00000000000..ec4ca31f837
--- /dev/null
+++ b/changelogs/unreleased/31912-epic-labels.yml
@@ -0,0 +1,5 @@
+---
+title: Manage and display labels from epic in the GraphQL API
+merge_request: 19642
+author:
+type: added
diff --git a/changelogs/unreleased/35534-broken-scroll-to-bottom.yml b/changelogs/unreleased/35534-broken-scroll-to-bottom.yml
new file mode 100644
index 00000000000..a7b6d06a8f8
--- /dev/null
+++ b/changelogs/unreleased/35534-broken-scroll-to-bottom.yml
@@ -0,0 +1,5 @@
+---
+title: Fix scroll to bottom with new job log
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/infinite-scroll.yml b/changelogs/unreleased/infinite-scroll.yml
new file mode 100644
index 00000000000..825eaacad4d
--- /dev/null
+++ b/changelogs/unreleased/infinite-scroll.yml
@@ -0,0 +1,5 @@
+---
+title: Add Infinite scroll to Add Projects modal in the operations dashboard
+merge_request: 17842
+author:
+type: fixed
diff --git a/changelogs/unreleased/nfriend-edit-release-ux-cleanup.yml b/changelogs/unreleased/nfriend-edit-release-ux-cleanup.yml
new file mode 100644
index 00000000000..f593db40382
--- /dev/null
+++ b/changelogs/unreleased/nfriend-edit-release-ux-cleanup.yml
@@ -0,0 +1,5 @@
+---
+title: Update help text of "Tag name" field on Edit Release page
+merge_request: 19321
+author:
+type: changed
diff --git a/db/migrate/20191030135044_create_plan_limits.rb b/db/migrate/20191030135044_create_plan_limits.rb
new file mode 100644
index 00000000000..291d9824f6d
--- /dev/null
+++ b/db/migrate/20191030135044_create_plan_limits.rb
@@ -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
diff --git a/db/migrate/20191030152934_move_limits_from_plans.rb b/db/migrate/20191030152934_move_limits_from_plans.rb
new file mode 100644
index 00000000000..020a028f648
--- /dev/null
+++ b/db/migrate/20191030152934_move_limits_from_plans.rb
@@ -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
diff --git a/db/post_migrate/20191030193050_remove_pendo_from_application_settings.rb b/db/post_migrate/20191030193050_remove_pendo_from_application_settings.rb
new file mode 100644
index 00000000000..33bbe6f8ea7
--- /dev/null
+++ b/db/post_migrate/20191030193050_remove_pendo_from_application_settings.rb
@@ -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
diff --git a/db/post_migrate/20191031112603_remove_limits_from_plans.rb b/db/post_migrate/20191031112603_remove_limits_from_plans.rb
new file mode 100644
index 00000000000..30fb6a9d193
--- /dev/null
+++ b/db/post_migrate/20191031112603_remove_limits_from_plans.rb
@@ -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
diff --git a/db/schema.rb b/db/schema.rb
index 0c816d4764e..237febdef25 100644
--- a/db/schema.rb
+++ b/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
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index d7dd5774365..723294da35e 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -217,6 +217,11 @@ 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.
"""
clientMutationId: String
@@ -242,6 +247,11 @@ input CreateEpicInput {
groupPath: ID!
"""
+ The IDs of labels to be removed from the epic.
+ """
+ removeLabelIds: [ID!]
+
+ """
The start date of the epic
"""
startDateFixed: String
@@ -1172,6 +1182,31 @@ type Epic implements Noteable {
): 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
"""
notes(
@@ -5062,6 +5097,11 @@ 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.
"""
clientMutationId: String
@@ -5092,6 +5132,11 @@ input UpdateEpicInput {
iid: String!
"""
+ The IDs of labels to be removed from the epic.
+ """
+ removeLabelIds: [ID!]
+
+ """
The start date of the epic
"""
startDateFixed: String
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index da8ff669b72..f921f1ef45c 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -3789,6 +3789,59 @@
"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",
"args": [
@@ -6251,6 +6304,213 @@
},
{
"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",
"description": "The connection type for User.",
"fields": [
@@ -7484,213 +7744,6 @@
},
{
"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",
"description": null,
"fields": [
@@ -17013,6 +17066,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",
"type": {
@@ -17222,6 +17311,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.",
"type": {
diff --git a/doc/api/settings.md b/doc/api/settings.md
index 16624d56a90..0fb742982f4 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -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). |
diff --git a/doc/development/event_tracking/index.md b/doc/development/event_tracking/index.md
index efc61d13cb0..ac19053320d 100644
--- a/doc/development/event_tracking/index.md
+++ b/doc/development/event_tracking/index.md
@@ -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.
diff --git a/doc/user/project/pages/img/new_project_for_pages_v12_5.png b/doc/user/project/pages/img/new_project_for_pages_v12_5.png
new file mode 100644
index 00000000000..8d2dc0bf9f5
--- /dev/null
+++ b/doc/user/project/pages/img/new_project_for_pages_v12_5.png
Binary files differ
diff --git a/doc/user/project/pages/img/pages_workflow_v12_5.png b/doc/user/project/pages/img/pages_workflow_v12_5.png
new file mode 100644
index 00000000000..ca5190fca79
--- /dev/null
+++ b/doc/user/project/pages/img/pages_workflow_v12_5.png
Binary files differ
diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md
index d1698cd54a1..abd67c90dd6 100644
--- a/doc/user/project/pages/index.md
+++ b/doc/user/project/pages/index.md
@@ -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).
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index 7bf09fd85e2..0669f764d4d 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -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",
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 6dc257ec67a..0bd1b6411ea 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/spec/frontend/notes/components/diff_discussion_header_spec.js b/spec/frontend/notes/components/diff_discussion_header_spec.js
new file mode 100644
index 00000000000..f90147f9105
--- /dev/null
+++ b/spec/frontend/notes/components/diff_discussion_header_spec.js
@@ -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();
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/releases/detail/components/app_spec.js b/spec/frontend/releases/detail/components/app_spec.js
index f8eb33a69a8..4726f18c8fa 100644
--- a/spec/frontend/releases/detail/components/app_spec.js
+++ b/spec/frontend/releases/detail/components/app_spec.js
@@ -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);
});
diff --git a/spec/frontend/repository/pages/index_spec.js b/spec/frontend/repository/pages/index_spec.js
new file mode 100644
index 00000000000..c0afb7931b1
--- /dev/null
+++ b/spec/frontend/repository/pages/index_spec.js
@@ -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: '/' });
+ });
+});
diff --git a/spec/frontend/repository/pages/tree_spec.js b/spec/frontend/repository/pages/tree_spec.js
new file mode 100644
index 00000000000..36662696c91
--- /dev/null
+++ b/spec/frontend/repository/pages/tree_spec.js
@@ -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],
+ ]);
+ });
+ });
+});
diff --git a/spec/frontend/repository/utils/dom_spec.js b/spec/frontend/repository/utils/dom_spec.js
new file mode 100644
index 00000000000..678d444904d
--- /dev/null
+++ b/spec/frontend/repository/utils/dom_spec.js
@@ -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');
+ });
+});
diff --git a/spec/frontend/repository/utils/title_spec.js b/spec/frontend/repository/utils/title_spec.js
index c4879716fd7..63035933424 100644
--- a/spec/frontend/repository/utils/title_spec.js
+++ b/spec/frontend/repository/utils/title_spec.js
@@ -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`);
});
});
diff --git a/spec/helpers/application_settings_helper_spec.rb b/spec/helpers/application_settings_helper_spec.rb
index a5e8370a715..8303c4eafbe 100644
--- a/spec/helpers/application_settings_helper_spec.rb
+++ b/spec/helpers/application_settings_helper_spec.rb
@@ -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
diff --git a/spec/helpers/releases_helper_spec.rb b/spec/helpers/releases_helper_spec.rb
index 3b4973677ef..3f56c189642 100644
--- a/spec/helpers/releases_helper_spec.rb
+++ b/spec/helpers/releases_helper_spec.rb
@@ -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
diff --git a/spec/javascripts/frequent_items/components/app_spec.js b/spec/javascripts/frequent_items/components/app_spec.js
index 36dd8604d08..da0427d650a 100644
--- a/spec/javascripts/frequent_items/components/app_spec.js
+++ b/spec/javascripts/frequent_items/components/app_spec.js
@@ -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)
diff --git a/spec/javascripts/frequent_items/mock_data.js b/spec/javascripts/frequent_items/mock_data.js
index 3ca5b4c7446..7f7d7b1cdbf 100644
--- a/spec/javascripts/frequent_items/mock_data.js
+++ b/spec/javascripts/frequent_items/mock_data.js
@@ -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 = [
diff --git a/spec/javascripts/frequent_items/store/actions_spec.js b/spec/javascripts/frequent_items/store/actions_spec.js
index 0a8525e77d6..7b065b69cce 100644
--- a/spec/javascripts/frequent_items/store/actions_spec.js
+++ b/spec/javascripts/frequent_items/store/actions_spec.js
@@ -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,
);
diff --git a/spec/javascripts/notes/components/noteable_discussion_spec.js b/spec/javascripts/notes/components/noteable_discussion_spec.js
index ea5c57b8a7c..ea1ed3da112 100644
--- a/spec/javascripts/notes/components/noteable_discussion_spec.js
+++ b/spec/javascripts/notes/components/noteable_discussion_spec.js
@@ -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)
diff --git a/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js b/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js
index 9c2deca585b..323a0f03017 100644
--- a/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js
+++ b/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js
@@ -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));
diff --git a/spec/migrations/move_limits_from_plans_spec.rb b/spec/migrations/move_limits_from_plans_spec.rb
new file mode 100644
index 00000000000..693d6ecb2c1
--- /dev/null
+++ b/spec/migrations/move_limits_from_plans_spec.rb
@@ -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
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 9d3f5b4b132..4aa8f2d959d 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -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)
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 1b58fb1dab1..c50cb4a5927 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -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) }
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 21e308e6636..604befd7225 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -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] }