diff options
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 Binary files differnew file mode 100644 index 00000000000..8d2dc0bf9f5 --- /dev/null +++ b/doc/user/project/pages/img/new_project_for_pages_v12_5.png diff --git a/doc/user/project/pages/img/pages_workflow_v12_5.png b/doc/user/project/pages/img/pages_workflow_v12_5.png Binary files differnew file mode 100644 index 00000000000..ca5190fca79 --- /dev/null +++ b/doc/user/project/pages/img/pages_workflow_v12_5.png 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] } |