diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-10 09:09:01 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-10 09:09:01 +0000 |
commit | cdd71cf36a45b72d8207fe4fcfc4e44a405d3607 (patch) | |
tree | f476a58abd1b6bbb44f9dbbc2fa6aa483f2c65a3 | |
parent | ff1701e51d8eac96e371de168c582b964ea96a83 (diff) | |
download | gitlab-ce-cdd71cf36a45b72d8207fe4fcfc4e44a405d3607.tar.gz |
Add latest changes from gitlab-org/gitlab@master
61 files changed, 990 insertions, 718 deletions
diff --git a/app/assets/javascripts/issuables_list/components/issuable.vue b/app/assets/javascripts/issuables_list/components/issuable.vue index 9af1887ef12..04991a8d374 100644 --- a/app/assets/javascripts/issuables_list/components/issuable.vue +++ b/app/assets/javascripts/issuables_list/components/issuable.vue @@ -365,7 +365,7 @@ export default { :title="__('Comments')" :class="{ 'no-comments': hasNoComments }" > - <i class="fa fa-comments"></i> + <gl-icon name="comments" class="gl-vertical-align-text-bottom" /> {{ userNotesCount }} </gl-link> </div> diff --git a/app/assets/javascripts/issuables_list/components/issuables_list_app.vue b/app/assets/javascripts/issuables_list/components/issuables_list_app.vue index db18bcbce09..e1a40323f5d 100644 --- a/app/assets/javascripts/issuables_list/components/issuables_list_app.vue +++ b/app/assets/javascripts/issuables_list/components/issuables_list_app.vue @@ -12,8 +12,10 @@ import { import { __ } from '~/locale'; import initManualOrdering from '~/manual_ordering'; import Issuable from './issuable.vue'; +import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; import { sortOrderMap, + availableSortOptionsJira, RELATIVE_POSITION, PAGE_SIZE, PAGE_SIZE_MANUAL, @@ -29,6 +31,7 @@ export default { GlPagination, GlSkeletonLoading, Issuable, + FilteredSearchBar, }, props: { canBulkEdit: { @@ -50,14 +53,25 @@ export default { type: String, required: true, }, + projectPath: { + type: String, + required: false, + default: '', + }, sortKey: { type: String, required: false, default: '', }, + type: { + type: String, + required: false, + default: '', + }, }, data() { return { + availableSortOptionsJira, filters: {}, isBulkEditing: false, issuables: [], @@ -141,6 +155,22 @@ export default { nextPage: this.paginationNext, }; }, + isJira() { + return this.type === 'jira'; + }, + initialFilterValue() { + const value = []; + const { search } = this.getQueryObject(); + + if (search) { + value.push(search); + } + return value; + }, + initialSortBy() { + const { sort } = this.getQueryObject(); + return sort || 'created_desc'; + }, }, watch: { selection() { @@ -262,51 +292,92 @@ export default { this.filters = filters; }, + refetchIssuables() { + const ignored = ['utf8', 'state']; + const params = omit(this.filters, ignored); + + historyPushState(setUrlParams(params, window.location.href, true)); + this.fetchIssuables(); + }, + handleFilter(filters) { + let search = null; + + filters.forEach(filter => { + if (typeof filter === 'string') { + search = filter; + } + }); + + this.filters.search = search; + this.page = 1; + + this.refetchIssuables(); + }, + handleSort(sort) { + this.filters.sort = sort; + this.page = 1; + + this.refetchIssuables(); + }, }, }; </script> <template> - <ul v-if="loading" class="content-list"> - <li v-for="n in $options.LOADING_LIST_ITEMS_LENGTH" :key="n" class="issue gl-px-5! gl-py-5!"> - <gl-skeleton-loading /> - </li> - </ul> - <div v-else-if="issuables.length"> - <div v-if="isBulkEditing" class="issue px-3 py-3 border-bottom border-light"> - <input type="checkbox" :checked="allIssuablesSelected" class="mr-2" @click="onSelectAll" /> - <strong>{{ __('Select all') }}</strong> - </div> - <ul - class="content-list issuable-list issues-list" - :class="{ 'manual-ordering': isManualOrdering }" - > - <issuable - v-for="issuable in issuables" - :key="issuable.id" - class="pr-3" - :class="{ 'user-can-drag': isManualOrdering }" - :issuable="issuable" - :is-bulk-editing="isBulkEditing" - :selected="isSelected(issuable.id)" - :base-url="baseUrl" - @select="onSelectIssuable" - /> + <div> + <filtered-search-bar + v-if="isJira" + :namespace="projectPath" + :search-input-placeholder="__('Search Jira issues')" + :tokens="[]" + :sort-options="availableSortOptionsJira" + :initial-filter-value="initialFilterValue" + :initial-sort-by="initialSortBy" + class="row-content-block" + @onFilter="handleFilter" + @onSort="handleSort" + /> + <ul v-if="loading" class="content-list"> + <li v-for="n in $options.LOADING_LIST_ITEMS_LENGTH" :key="n" class="issue gl-px-5! gl-py-5!"> + <gl-skeleton-loading /> + </li> </ul> - <div class="mt-3"> - <gl-pagination - v-bind="paginationProps" - class="gl-justify-content-center" - @input="onPaginate" - /> + <div v-else-if="issuables.length"> + <div v-if="isBulkEditing" class="issue px-3 py-3 border-bottom border-light"> + <input type="checkbox" :checked="allIssuablesSelected" class="mr-2" @click="onSelectAll" /> + <strong>{{ __('Select all') }}</strong> + </div> + <ul + class="content-list issuable-list issues-list" + :class="{ 'manual-ordering': isManualOrdering }" + > + <issuable + v-for="issuable in issuables" + :key="issuable.id" + class="pr-3" + :class="{ 'user-can-drag': isManualOrdering }" + :issuable="issuable" + :is-bulk-editing="isBulkEditing" + :selected="isSelected(issuable.id)" + :base-url="baseUrl" + @select="onSelectIssuable" + /> + </ul> + <div class="mt-3"> + <gl-pagination + v-bind="paginationProps" + class="gl-justify-content-center" + @input="onPaginate" + /> + </div> </div> + <gl-empty-state + v-else + :title="emptyState.title" + :description="emptyState.description" + :svg-path="emptySvgPath" + :primary-button-link="emptyState.primaryLink" + :primary-button-text="emptyState.primaryText" + /> </div> - <gl-empty-state - v-else - :title="emptyState.title" - :description="emptyState.description" - :svg-path="emptySvgPath" - :primary-button-link="emptyState.primaryLink" - :primary-button-text="emptyState.primaryText" - /> </template> diff --git a/app/assets/javascripts/issuables_list/constants.js b/app/assets/javascripts/issuables_list/constants.js index 71b9c52c703..e240efd2804 100644 --- a/app/assets/javascripts/issuables_list/constants.js +++ b/app/assets/javascripts/issuables_list/constants.js @@ -1,3 +1,5 @@ +import { __ } from '~/locale'; + // Maps sort order as it appears in the URL query to API `order_by` and `sort` params. const PRIORITY = 'priority'; const ASC = 'asc'; @@ -31,3 +33,22 @@ export const sortOrderMap = { weight_desc: { order_by: WEIGHT, sort: DESC }, weight: { order_by: WEIGHT, sort: ASC }, }; + +export const availableSortOptionsJira = [ + { + id: 1, + title: __('Created date'), + sortDirection: { + descending: 'created_desc', + ascending: 'created_asc', + }, + }, + { + id: 2, + title: __('Last updated'), + sortDirection: { + descending: 'updated_desc', + ascending: 'updated_asc', + }, + }, +]; diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index b742fe42024..3f85295a5ed 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -33,7 +33,7 @@ import initFrequentItemDropdowns from './frequent_items'; import initBreadcrumbs from './breadcrumb'; import initUsagePingConsent from './usage_ping_consent'; import initPerformanceBar from './performance_bar'; -import initGlobalSearchInput from './global_search_input'; +import initSearchAutocomplete from './search_autocomplete'; import GlFieldErrors from './gl_field_errors'; import initUserPopovers from './user_popovers'; import initBroadcastNotifications from './broadcast_notification'; @@ -113,7 +113,7 @@ function deferredInitialisation() { initFrequentItemDropdowns(); initPersistentUserCallouts(); - if (document.querySelector('.search')) initGlobalSearchInput(); + if (document.querySelector('.search')) initSearchAutocomplete(); addSelectOnFocusBehaviour('.js-select-on-focus'); diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 32af62fe6f1..8a7734f4d31 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -402,9 +402,8 @@ export const saveNote = ({ commit, dispatch }, noteData) => { }; const pollSuccessCallBack = (resp, commit, state, getters, dispatch) => { - if (resp.notes && resp.notes.length) { - updateOrCreateNotes({ commit, state, getters, dispatch }, resp.notes); - + if (resp.notes?.length) { + dispatch('updateOrCreateNotes', resp.notes); dispatch('startTaskList'); } @@ -424,12 +423,12 @@ const getFetchDataParams = state => { return { endpoint, options }; }; -export const fetchData = ({ commit, state, getters }) => { +export const fetchData = ({ commit, state, getters, dispatch }) => { const { endpoint, options } = getFetchDataParams(state); axios .get(endpoint, options) - .then(({ data }) => pollSuccessCallBack(data, commit, state, getters)) + .then(({ data }) => pollSuccessCallBack(data, commit, state, getters, dispatch)) .catch(() => Flash(__('Something went wrong while fetching latest comments.'))); }; @@ -449,7 +448,7 @@ export const poll = ({ commit, state, getters, dispatch }) => { if (!Visibility.hidden()) { eTagPoll.makeRequest(); } else { - fetchData({ commit, state, getters }); + dispatch('fetchData'); } Visibility.change(() => { diff --git a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue b/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue index d9816dc5102..51ba2337db6 100644 --- a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue +++ b/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue @@ -1,5 +1,5 @@ <script> -import { GlFormCheckbox, GlTooltipDirective, GlSprintf } from '@gitlab/ui'; +import { GlFormCheckbox, GlTooltipDirective, GlSprintf, GlIcon } from '@gitlab/ui'; import { n__ } from '~/locale'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import { numberToHumanSize } from '~/lib/utils/number_utils'; @@ -16,12 +16,16 @@ import { PUBLISHED_DETAILS_ROW_TEXT, MANIFEST_DETAILS_ROW_TEST, CONFIGURATION_DETAILS_ROW_TEST, + MISSING_MANIFEST_WARNING_TOOLTIP, + NOT_AVAILABLE_TEXT, + NOT_AVAILABLE_SIZE, } from '../../constants/index'; export default { components: { GlSprintf, GlFormCheckbox, + GlIcon, DeleteButton, ListItem, ClipboardButton, @@ -55,10 +59,11 @@ export default { PUBLISHED_DETAILS_ROW_TEXT, MANIFEST_DETAILS_ROW_TEST, CONFIGURATION_DETAILS_ROW_TEST, + MISSING_MANIFEST_WARNING_TOOLTIP, }, computed: { formattedSize() { - return this.tag.total_size ? numberToHumanSize(this.tag.total_size) : ''; + return this.tag.total_size ? numberToHumanSize(this.tag.total_size) : NOT_AVAILABLE_SIZE; }, layers() { return this.tag.layers ? n__('%d layer', '%d layers', this.tag.layers) : ''; @@ -68,7 +73,7 @@ export default { }, shortDigest() { // remove sha256: from the string, and show only the first 7 char - return this.tag.digest?.substring(7, 14); + return this.tag.digest?.substring(7, 14) ?? NOT_AVAILABLE_TEXT; }, publishedDate() { return formatDate(this.tag.created_at, 'isoDate'); @@ -85,6 +90,9 @@ export default { tagLocation() { return this.tag.path?.replace(`:${this.tag.name}`, ''); }, + invalidTag() { + return !this.tag.digest; + }, }, }; </script> @@ -94,6 +102,7 @@ export default { <template #left-action> <gl-form-checkbox v-if="Boolean(tag.destroy_path)" + :disabled="invalidTag" class="gl-m-0" :checked="selected" @change="$emit('select')" @@ -116,6 +125,13 @@ export default { :text="tag.location" css-class="btn-default btn-transparent btn-clipboard" /> + + <gl-icon + v-if="invalidTag" + v-gl-tooltip="{ title: $options.i18n.MISSING_MANIFEST_WARNING_TOOLTIP }" + name="warning" + class="gl-text-orange-500 gl-mb-2 gl-ml-2" + /> </div> </template> @@ -146,7 +162,7 @@ export default { </template> <template #right-action> <delete-button - :disabled="!tag.destroy_path" + :disabled="!tag.destroy_path || invalidTag" :title="$options.i18n.REMOVE_TAG_BUTTON_TITLE" :tooltip-title="$options.i18n.REMOVE_TAG_BUTTON_DISABLE_TOOLTIP" :tooltip-disabled="Boolean(tag.destroy_path)" @@ -154,7 +170,8 @@ export default { @delete="$emit('delete')" /> </template> - <template #details_published> + + <template v-if="!invalidTag" #details_published> <details-row icon="clock" data-testid="published-date-detail"> <gl-sprintf :message="$options.i18n.PUBLISHED_DETAILS_ROW_TEXT"> <template #repositoryPath> @@ -169,7 +186,7 @@ export default { </gl-sprintf> </details-row> </template> - <template #details_manifest_digest> + <template v-if="!invalidTag" #details_manifest_digest> <details-row icon="log" data-testid="manifest-detail"> <gl-sprintf :message="$options.i18n.MANIFEST_DETAILS_ROW_TEST"> <template #digest> @@ -184,7 +201,7 @@ export default { /> </details-row> </template> - <template #details_configuration_digest> + <template v-if="!invalidTag" #details_configuration_digest> <details-row icon="cloud-gear" data-testid="configuration-detail"> <gl-sprintf :message="$options.i18n.CONFIGURATION_DETAILS_ROW_TEST"> <template #digest> diff --git a/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue b/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue index e62c0b78efb..2874d89d913 100644 --- a/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue +++ b/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue @@ -88,7 +88,7 @@ export default { v-if="item.failedDelete" v-gl-tooltip="{ title: $options.i18n.ASYNC_DELETE_IMAGE_ERROR_MESSAGE }" name="warning" - class="text-warning" + class="gl-text-orange-500" /> </template> <template #left-secondary> diff --git a/app/assets/javascripts/registry/explorer/constants/details.js b/app/assets/javascripts/registry/explorer/constants/details.js index c6b611114e6..1dc5882d415 100644 --- a/app/assets/javascripts/registry/explorer/constants/details.js +++ b/app/assets/javascripts/registry/explorer/constants/details.js @@ -1,4 +1,4 @@ -import { s__ } from '~/locale'; +import { s__, __ } from '~/locale'; // Translations strings export const DETAILS_PAGE_TITLE = s__('ContainerRegistry|%{imageName} tags'); @@ -48,6 +48,12 @@ export const REMOVE_TAG_BUTTON_DISABLE_TOOLTIP = s__( 'ContainerRegistry|Deletion disabled due to missing or insufficient permissions.', ); +export const MISSING_MANIFEST_WARNING_TOOLTIP = s__( + 'ContainerRegistry|Invalid tag: missing manifest digest', +); + +export const NOT_AVAILABLE_TEXT = __('N/A'); +export const NOT_AVAILABLE_SIZE = __('0 bytes'); // Parameters export const DEFAULT_PAGE = 1; diff --git a/app/assets/javascripts/global_search_input.js b/app/assets/javascripts/search_autocomplete.js index a7c121259d4..05e0b9e7089 100644 --- a/app/assets/javascripts/global_search_input.js +++ b/app/assets/javascripts/search_autocomplete.js @@ -1,8 +1,10 @@ /* eslint-disable no-return-assign, consistent-return, class-methods-use-this */ import $ from 'jquery'; -import { throttle } from 'lodash'; +import { escape, throttle } from 'lodash'; import { s__, __, sprintf } from '~/locale'; +import { getIdenticonBackgroundClass, getIdenticonTitle } from '~/helpers/avatar_helper'; +import axios from './lib/utils/axios_utils'; import { isInGroupsPage, isInProjectPage, @@ -65,11 +67,15 @@ function setSearchOptions() { } } -export class GlobalSearchInput { - constructor({ wrap } = {}) { +export class SearchAutocomplete { + constructor({ wrap, optsEl, autocompletePath, projectId, projectRef } = {}) { setSearchOptions(); this.bindEventContext(); this.wrap = wrap || $('.search'); + this.optsEl = optsEl || this.wrap.find('.search-autocomplete-opts'); + this.autocompletePath = autocompletePath || this.optsEl.data('autocompletePath'); + this.projectId = projectId || (this.optsEl.data('autocompleteProjectId') || ''); + this.projectRef = projectRef || (this.optsEl.data('autocompleteProjectRef') || ''); this.dropdown = this.wrap.find('.dropdown'); this.dropdownToggle = this.wrap.find('.js-dropdown-search-toggle'); this.dropdownMenu = this.dropdown.find('.dropdown-menu'); @@ -86,7 +92,7 @@ export class GlobalSearchInput { // Only when user is logged in if (gon.current_user_id) { - this.createGlobalSearchInput(); + this.createAutocomplete(); } this.bindEvents(); @@ -111,7 +117,7 @@ export class GlobalSearchInput { return (this.originalState = this.serializeState()); } - createGlobalSearchInput() { + createAutocomplete() { return this.searchInput.glDropdown({ filterInputBlur: false, filterable: true, @@ -143,17 +149,116 @@ export class GlobalSearchInput { if (glDropdownInstance) { glDropdownInstance.filter.options.callback(contents); } - this.enableDropdown(); + this.enableAutocomplete(); } return; } - const options = this.scopedSearchOptions(term); + // Prevent multiple ajax calls + if (this.loadingSuggestions) { + return; + } - callback(options); + this.loadingSuggestions = true; + + return axios + .get(this.autocompletePath, { + params: { + project_id: this.projectId, + project_ref: this.projectRef, + term, + }, + }) + .then(response => { + const options = this.scopedSearchOptions(term); + + // List results + let lastCategory = null; + for (let i = 0, len = response.data.length; i < len; i += 1) { + const suggestion = response.data[i]; + // Add group header before list each group + if (lastCategory !== suggestion.category) { + options.push({ type: 'separator' }); + options.push({ + type: 'header', + content: suggestion.category, + }); + lastCategory = suggestion.category; + } + + // Add the suggestion + options.push({ + id: `${suggestion.category.toLowerCase()}-${suggestion.id}`, + icon: this.getAvatar(suggestion), + category: suggestion.category, + text: suggestion.label, + url: suggestion.url, + }); + } - this.highlightFirstRow(); - this.setScrollFade(); + callback(options); + + this.loadingSuggestions = false; + this.highlightFirstRow(); + this.setScrollFade(); + }) + .catch(() => { + this.loadingSuggestions = false; + }); + } + + getCategoryContents() { + const userName = gon.current_username; + const { projectOptions, groupOptions, dashboardOptions } = gl; + + // Get options + let options; + if (isInProjectPage() && projectOptions) { + options = projectOptions[getProjectSlug()]; + } else if (isInGroupsPage() && groupOptions) { + options = groupOptions[getGroupSlug()]; + } else if (dashboardOptions) { + options = dashboardOptions; + } + + const { issuesPath, mrPath, name, issuesDisabled } = options; + const baseItems = []; + + if (name) { + baseItems.push({ + type: 'header', + content: `${name}`, + }); + } + + const issueItems = [ + { + text: s__('SearchAutocomplete|Issues assigned to me'), + url: `${issuesPath}/?assignee_username=${userName}`, + }, + { + text: s__("SearchAutocomplete|Issues I've created"), + url: `${issuesPath}/?author_username=${userName}`, + }, + ]; + const mergeRequestItems = [ + { + text: s__('SearchAutocomplete|Merge requests assigned to me'), + url: `${mrPath}/?assignee_username=${userName}`, + }, + { + text: s__("SearchAutocomplete|Merge requests I've created"), + url: `${mrPath}/?author_username=${userName}`, + }, + ]; + + let items; + if (issuesDisabled) { + items = baseItems.concat(mergeRequestItems); + } else { + items = baseItems.concat(...issueItems, ...mergeRequestItems); + } + return items; } // Add option to proceed with the search for each @@ -238,7 +343,7 @@ export class GlobalSearchInput { }); } - enableDropdown() { + enableAutocomplete() { this.setScrollFade(); // No need to enable anything if user is not logged in @@ -255,7 +360,7 @@ export class GlobalSearchInput { } onSearchInputChange() { - this.enableDropdown(); + this.enableAutocomplete(); } onSearchInputKeyUp(e) { @@ -264,7 +369,7 @@ export class GlobalSearchInput { this.restoreOriginalState(); break; case KEYCODE.ENTER: - this.disableDropdown(); + this.disableAutocomplete(); break; default: } @@ -317,7 +422,7 @@ export class GlobalSearchInput { return results; } - disableDropdown() { + disableAutocomplete() { if (!this.searchInput.hasClass('js-autocomplete-disabled') && this.dropdown.hasClass('show')) { this.searchInput.addClass('js-autocomplete-disabled'); this.dropdownToggle.dropdown('toggle'); @@ -333,8 +438,16 @@ export class GlobalSearchInput { onClick(item, $el, e) { if (window.location.pathname.indexOf(item.url) !== -1) { if (!e.metaKey) e.preventDefault(); + /* eslint-disable-next-line @gitlab/require-i18n-strings */ + if (item.category === 'Projects') { + this.projectInputEl.val(item.id); + } + // eslint-disable-next-line @gitlab/require-i18n-strings + if (item.category === 'Groups') { + this.groupInputEl.val(item.id); + } $el.removeClass('is-active'); - this.disableDropdown(); + this.disableAutocomplete(); return this.searchInput.val('').focus(); } } @@ -343,58 +456,20 @@ export class GlobalSearchInput { this.searchInput.data('glDropdown').highlightRowAtIndex(null, 0); } - getCategoryContents() { - const userName = gon.current_username; - const { projectOptions, groupOptions, dashboardOptions } = gl; - - // Get options - let options; - if (isInProjectPage() && projectOptions) { - options = projectOptions[getProjectSlug()]; - } else if (isInGroupsPage() && groupOptions) { - options = groupOptions[getGroupSlug()]; - } else if (dashboardOptions) { - options = dashboardOptions; + getAvatar(item) { + if (!Object.hasOwnProperty.call(item, 'avatar_url')) { + return false; } - const { issuesPath, mrPath, name, issuesDisabled } = options; - const baseItems = []; - - if (name) { - baseItems.push({ - type: 'header', - content: `${name}`, - }); - } + const { label, id } = item; + const avatarUrl = item.avatar_url; + const avatar = avatarUrl + ? `<img class="search-item-avatar" src="${avatarUrl}" />` + : `<div class="s16 avatar identicon ${getIdenticonBackgroundClass(id)}">${getIdenticonTitle( + escape(label), + )}</div>`; - const issueItems = [ - { - text: s__('SearchAutocomplete|Issues assigned to me'), - url: `${issuesPath}/?assignee_username=${userName}`, - }, - { - text: s__("SearchAutocomplete|Issues I've created"), - url: `${issuesPath}/?author_username=${userName}`, - }, - ]; - const mergeRequestItems = [ - { - text: s__('SearchAutocomplete|Merge requests assigned to me'), - url: `${mrPath}/?assignee_username=${userName}`, - }, - { - text: s__("SearchAutocomplete|Merge requests I've created"), - url: `${mrPath}/?author_username=${userName}`, - }, - ]; - - let items; - if (issuesDisabled) { - items = baseItems.concat(mergeRequestItems); - } else { - items = baseItems.concat(...issueItems, ...mergeRequestItems); - } - return items; + return avatar; } isScrolledUp() { @@ -420,6 +495,6 @@ export class GlobalSearchInput { } } -export default function initGlobalSearchInput(opts) { - return new GlobalSearchInput(opts); +export default function initSearchAutocomplete(opts) { + return new SearchAutocomplete(opts); } diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 217f08dd648..ff6d9350a5c 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -51,6 +51,21 @@ class SearchController < ApplicationController render json: { count: count } end + # rubocop: disable CodeReuse/ActiveRecord + def autocomplete + term = params[:term] + + if params[:project_id].present? + @project = Project.find_by(id: params[:project_id]) + @project = nil unless can?(current_user, :read_project, @project) + end + + @ref = params[:project_ref] if params[:project_ref].present? + + render json: search_autocomplete_opts(term).to_json + end + # rubocop: enable CodeReuse/ActiveRecord + private def preload_method diff --git a/app/helpers/export_helper.rb b/app/helpers/export_helper.rb index 483b350b99b..38a4f7f1b4b 100644 --- a/app/helpers/export_helper.rb +++ b/app/helpers/export_helper.rb @@ -6,7 +6,7 @@ module ExportHelper [ _('Project and wiki repositories'), _('Project uploads'), - _('Project configuration, including services'), + _('Project configuration, excluding integrations'), _('Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities'), _('LFS objects'), _('Issue Boards'), diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 58ce063dbd4..1b9876b9a6a 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -3,6 +3,28 @@ module SearchHelper SEARCH_PERMITTED_PARAMS = [:search, :scope, :project_id, :group_id, :repository_ref, :snippets].freeze + def search_autocomplete_opts(term) + return unless current_user + + resources_results = [ + groups_autocomplete(term), + projects_autocomplete(term) + ].flatten + + search_pattern = Regexp.new(Regexp.escape(term), "i") + + generic_results = project_autocomplete + default_autocomplete + help_autocomplete + generic_results.concat(default_autocomplete_admin) if current_user.admin? + generic_results.select! { |result| result[:label] =~ search_pattern } + + [ + resources_results, + generic_results + ].flatten.uniq do |item| + item[:label] + end + end + def search_entries_info(collection, scope, term) return if collection.to_a.empty? @@ -73,6 +95,91 @@ module SearchHelper private + # Autocomplete results for various settings pages + def default_autocomplete + [ + { category: "Settings", label: _("User settings"), url: profile_path }, + { category: "Settings", label: _("SSH Keys"), url: profile_keys_path }, + { category: "Settings", label: _("Dashboard"), url: root_path } + ] + end + + # Autocomplete results for settings pages, for admins + def default_autocomplete_admin + [ + { category: "Settings", label: _("Admin Section"), url: admin_root_path } + ] + end + + # Autocomplete results for internal help pages + def help_autocomplete + [ + { category: "Help", label: _("API Help"), url: help_page_path("api/README") }, + { category: "Help", label: _("Markdown Help"), url: help_page_path("user/markdown") }, + { category: "Help", label: _("Permissions Help"), url: help_page_path("user/permissions") }, + { category: "Help", label: _("Public Access Help"), url: help_page_path("public_access/public_access") }, + { category: "Help", label: _("Rake Tasks Help"), url: help_page_path("raketasks/README") }, + { category: "Help", label: _("SSH Keys Help"), url: help_page_path("ssh/README") }, + { category: "Help", label: _("System Hooks Help"), url: help_page_path("system_hooks/system_hooks") }, + { category: "Help", label: _("Webhooks Help"), url: help_page_path("user/project/integrations/webhooks") }, + { category: "Help", label: _("Workflow Help"), url: help_page_path("workflow/README") } + ] + end + + # Autocomplete results for the current project, if it's defined + def project_autocomplete + if @project && @project.repository.root_ref + ref = @ref || @project.repository.root_ref + + [ + { category: "In this project", label: _("Files"), url: project_tree_path(@project, ref) }, + { category: "In this project", label: _("Commits"), url: project_commits_path(@project, ref) }, + { category: "In this project", label: _("Network"), url: project_network_path(@project, ref) }, + { category: "In this project", label: _("Graph"), url: project_graph_path(@project, ref) }, + { category: "In this project", label: _("Issues"), url: project_issues_path(@project) }, + { category: "In this project", label: _("Merge Requests"), url: project_merge_requests_path(@project) }, + { category: "In this project", label: _("Milestones"), url: project_milestones_path(@project) }, + { category: "In this project", label: _("Snippets"), url: project_snippets_path(@project) }, + { category: "In this project", label: _("Members"), url: project_project_members_path(@project) }, + { category: "In this project", label: _("Wiki"), url: project_wikis_path(@project) } + ] + else + [] + end + end + + # Autocomplete results for the current user's groups + # rubocop: disable CodeReuse/ActiveRecord + def groups_autocomplete(term, limit = 5) + current_user.authorized_groups.order_id_desc.search(term).limit(limit).map do |group| + { + category: "Groups", + id: group.id, + label: "#{search_result_sanitize(group.full_name)}", + url: group_path(group), + avatar_url: group.avatar_url || '' + } + end + end + # rubocop: enable CodeReuse/ActiveRecord + + # Autocomplete results for the current user's projects + # rubocop: disable CodeReuse/ActiveRecord + def projects_autocomplete(term, limit = 5) + current_user.authorized_projects.order_id_desc.search_by_title(term) + .sorted_by_stars_desc.non_archived.limit(limit).map do |p| + { + category: "Projects", + id: p.id, + value: "#{search_result_sanitize(p.name)}", + label: "#{search_result_sanitize(p.full_name)}", + url: project_path(p), + avatar_url: p.avatar_url || '' + } + end + end + # rubocop: enable CodeReuse/ActiveRecord + def search_result_sanitize(str) Sanitize.clean(str) end diff --git a/app/services/jira/requests/issues/list_service.rb b/app/services/jira/requests/issues/list_service.rb index 5752e77d16f..442bf6ba27a 100644 --- a/app/services/jira/requests/issues/list_service.rb +++ b/app/services/jira/requests/issues/list_service.rb @@ -12,8 +12,8 @@ module Jira super(jira_service, params) @jql = params[:jql].to_s - @page = params[:page].to_i || 1 - @per_page = params[:per_page].to_i || PER_PAGE + @page = (params[:page] || 1).to_i + @per_page = (params[:per_page] || PER_PAGE).to_i end private diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index 97d00bce11b..81fe0798bd1 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -2,7 +2,7 @@ = form_tag search_path, method: :get, class: 'form-inline' do |f| .search-input-container .search-input-wrap - .dropdown + .dropdown{ data: { url: search_autocomplete_path } } = search_field_tag 'search', nil, placeholder: _('Search or jump to…'), class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options', spellcheck: false, @@ -37,3 +37,6 @@ -# workaround for non-JS feature specs, see spec/support/helpers/search_helpers.rb - if ENV['RAILS_ENV'] == 'test' %noscript= button_tag 'Search' + .search-autocomplete-opts.hide{ :'data-autocomplete-path' => search_autocomplete_path, + :'data-autocomplete-project-id' => search_context.project.try(:id), + :'data-autocomplete-project-ref' => search_context.ref } diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml index 6717939d034..b67bc71941a 100644 --- a/app/views/search/results/_note.html.haml +++ b/app/views/search/results/_note.html.haml @@ -4,7 +4,7 @@ .search-result-row %h5.note-search-caption.str-truncated - %i.fa.fa-comment + = sprite_icon('comment', size: 16, css_class: 'gl-vertical-align-text-bottom') = link_to_member(project, note.author, avatar: false) - link_to_project = link_to(project.full_name, project) = _("commented on %{link_to_project}").html_safe % { link_to_project: link_to_project } diff --git a/app/views/shared/_issuable_meta_data.html.haml b/app/views/shared/_issuable_meta_data.html.haml index dee6dc1e078..d704eae2090 100644 --- a/app/views/shared/_issuable_meta_data.html.haml +++ b/app/views/shared/_issuable_meta_data.html.haml @@ -21,5 +21,5 @@ %li.issuable-comments.d-none.d-sm-block = link_to issuable_path, class: ['has-tooltip', ('no-comments' if note_count.zero?)], title: _('Comments') do - = icon('comments') + = sprite_icon('comments', size: 16, css_class: 'gl-vertical-align-text-bottom') = note_count diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml index 128ddbb8e8b..b2c9a74b177 100644 --- a/app/views/shared/snippets/_snippet.html.haml +++ b/app/views/shared/snippets/_snippet.html.haml @@ -11,7 +11,7 @@ %ul.controls %li = link_to gitlab_snippet_path(snippet, anchor: 'notes'), class: ('no-comments' if notes_count.zero?) do - = icon('comments') + = sprite_icon('comments', size: 16, css_class: 'gl-vertical-align-text-bottom') = notes_count %li %span.sr-only diff --git a/changelogs/unreleased/212811-adding-new-task-always-shows-error-something-went-wrong-while-fetc.yml b/changelogs/unreleased/212811-adding-new-task-always-shows-error-something-went-wrong-while-fetc.yml new file mode 100644 index 00000000000..fd33640f42f --- /dev/null +++ b/changelogs/unreleased/212811-adding-new-task-always-shows-error-something-went-wrong-while-fetc.yml @@ -0,0 +1,5 @@ +--- +title: Fix comment loading error in issues and merge requests +merge_request: 36043 +author: +type: fixed diff --git a/changelogs/unreleased/220342-remove-services-from-import-export.yml b/changelogs/unreleased/220342-remove-services-from-import-export.yml new file mode 100644 index 00000000000..1e5af27198b --- /dev/null +++ b/changelogs/unreleased/220342-remove-services-from-import-export.yml @@ -0,0 +1,5 @@ +--- +title: Exclude integrations (services) from Project Import/Export +merge_request: 35249 +author: +type: changed diff --git a/changelogs/unreleased/225187-replace-fa-comment-icons-with-gitlab-svg-comment-icon.yml b/changelogs/unreleased/225187-replace-fa-comment-icons-with-gitlab-svg-comment-icon.yml new file mode 100644 index 00000000000..0a21c5d6622 --- /dev/null +++ b/changelogs/unreleased/225187-replace-fa-comment-icons-with-gitlab-svg-comment-icon.yml @@ -0,0 +1,5 @@ +--- +title: Replace fa-comment / fa-comments icons with GitLab SVG +merge_request: 36206 +author: +type: changed diff --git a/changelogs/unreleased/227558-missing-digest-revision-and-short-revision-in-tags.yml b/changelogs/unreleased/227558-missing-digest-revision-and-short-revision-in-tags.yml new file mode 100644 index 00000000000..370d0688734 --- /dev/null +++ b/changelogs/unreleased/227558-missing-digest-revision-and-short-revision-in-tags.yml @@ -0,0 +1,5 @@ +--- +title: Add broken tag state to tags list items +merge_request: 36442 +author: +type: changed diff --git a/changelogs/unreleased/feature-secure-eslint-to-core.yml b/changelogs/unreleased/feature-secure-eslint-to-core.yml new file mode 100644 index 00000000000..d93ef151a6a --- /dev/null +++ b/changelogs/unreleased/feature-secure-eslint-to-core.yml @@ -0,0 +1,5 @@ +--- +title: Bring SAST to Core - eslint +merge_request: 36392 +author: +type: changed diff --git a/changelogs/unreleased/revert-bc8546a9.yml b/changelogs/unreleased/revert-bc8546a9.yml new file mode 100644 index 00000000000..45513d899a1 --- /dev/null +++ b/changelogs/unreleased/revert-bc8546a9.yml @@ -0,0 +1,5 @@ +--- +title: Restore the search autocomplete for groups/project/other +merge_request: 35983 +author: +type: other diff --git a/config/routes.rb b/config/routes.rb index 9739d8fe0ff..c823be6084d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -58,6 +58,7 @@ Rails.application.routes.draw do # Search get 'search' => 'search#show' + get 'search/autocomplete' => 'search#autocomplete', as: :search_autocomplete get 'search/count' => 'search#count', as: :search_count # JSON Web Token diff --git a/crowdin.yml b/crowdin.yml index 2861d34c941..27d9fba92b2 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,7 +1,9 @@ project_identifier: 'gitlab-ee' api_key_env: CROWDIN_API_KEY preserve_hierarchy: true -commit_message: "[skip ci]" +commit_message: | + + [skip ci] files: - source: /locale/gitlab.pot diff --git a/doc/administration/gitaly/praefect.md b/doc/administration/gitaly/praefect.md index 4d9a502fe74..88e59ba7ffc 100644 --- a/doc/administration/gitaly/praefect.md +++ b/doc/administration/gitaly/praefect.md @@ -35,8 +35,7 @@ The availability objectives for Gitaly clusters are: Writes are replicated asynchronously. Any writes that have not been replicated to the newly promoted primary are lost. - [Strong Consistency](https://gitlab.com/groups/gitlab-org/-/epics/1189) is - planned to improve this to "no loss". + [Strong consistency](#strong-consistency) can be used to improve this to "no loss". - **Recovery Time Objective (RTO):** Less than 10 seconds. @@ -877,6 +876,35 @@ Prometheus counter metric. It has two labels: They reflect configuration defined for this instance of Praefect. +## Strong consistency + +> Introduced in GitLab 13.1 in [alpha](https://about.gitlab.com/handbook/product/#alpha-beta-ga), disabled by default. + +Praefect guarantees eventual consistency by replicating all writes to secondary nodes +after the write to the primary Gitaly node has happened. + +Praefect can instead provide strong consistency by creating a transaction and writing +changes to all Gitaly nodes at once. Strong consistency is currently in +[alpha](https://about.gitlab.com/handbook/product/#alpha-beta-ga) and not enabled by +default. For more information, see the +[strong consistency epic](https://gitlab.com/groups/gitlab-org/-/epics/1189). + +To enable strong consistency: + +- In GitLab 13.2 and later, enable the `:gitaly_reference_transactions` feature flag. +- In GitLab 13.1, enable the `:gitaly_reference_transactions` and `:gitaly_hooks_rpc` + feature flags. + +Enabling feature flags requires [access to the Rails console](../feature_flags.md#start-the-gitlab-rails-console). +In the Rails console, enable or disable the flags as required. For example: + +```ruby +Feature.enable(:gitaly_reference_transactions) +``` + +To monitor strong consistency, use the `gitaly_praefect_transactions_total` and +`gitaly_praefect_transactions_delay_seconds` Prometheus counter metrics. + ## Automatic failover and leader election Praefect regularly checks the health of each backend Gitaly node. This diff --git a/doc/user/application_security/sast/index.md b/doc/user/application_security/sast/index.md index e1600579621..1e67236b448 100644 --- a/doc/user/application_security/sast/index.md +++ b/doc/user/application_security/sast/index.md @@ -80,13 +80,13 @@ The following table shows which languages, package managers and frameworks are s | Groovy ([Ant](https://ant.apache.org/), [Gradle](https://gradle.org/), [Maven](https://maven.apache.org/) and [SBT](https://www.scala-sbt.org/)) | [SpotBugs](https://spotbugs.github.io/) with the [find-sec-bugs](https://find-sec-bugs.github.io/) plugin | 11.3 (Gradle) & 11.9 (Ant, Maven, SBT) | | Helm Charts | [Kubesec](https://github.com/controlplaneio/kubesec) | 13.1 | | Java ([Ant](https://ant.apache.org/), [Gradle](https://gradle.org/), [Maven](https://maven.apache.org/) and [SBT](https://www.scala-sbt.org/)) | [SpotBugs](https://spotbugs.github.io/) with the [find-sec-bugs](https://find-sec-bugs.github.io/) plugin | 10.6 (Maven), 10.8 (Gradle) & 11.9 (Ant, SBT) | -| JavaScript | [ESLint security plugin](https://github.com/nodesecurity/eslint-plugin-security) | 11.8 | +| JavaScript | [ESLint security plugin](https://github.com/nodesecurity/eslint-plugin-security) | 11.8, moved to [GitLab Core](https://about.gitlab.com/pricing/) in 13.2 | | Kubernetes manifests | [Kubesec](https://github.com/controlplaneio/kubesec) | 12.6 | | Node.js | [NodeJsScan](https://github.com/ajinabraham/NodeJsScan) | 11.1 | | PHP | [phpcs-security-audit](https://github.com/FloeDesignTechnologies/phpcs-security-audit) | 10.8 | | Python ([pip](https://pip.pypa.io/en/stable/)) | [bandit](https://github.com/PyCQA/bandit) | 10.3 | | React | [ESLint react plugin](https://github.com/yannickcr/eslint-plugin-react) | 12.5 | -| Ruby on Rails | [brakeman](https://brakemanscanner.org) | 10.3, moved to Core in 13.1 | +| Ruby on Rails | [brakeman](https://brakemanscanner.org) | 10.3, moved to [GitLab Core](https://about.gitlab.com/pricing/) in 13.1 | | Scala ([Ant](https://ant.apache.org/), [Gradle](https://gradle.org/), [Maven](https://maven.apache.org/) and [SBT](https://www.scala-sbt.org/)) | [SpotBugs](https://spotbugs.github.io/) with the [find-sec-bugs](https://find-sec-bugs.github.io/) plugin | 11.0 (SBT) & 11.9 (Ant, Gradle, Maven) | | TypeScript | [`tslint-config-security`](https://github.com/webschik/tslint-config-security/) | 11.9 | @@ -97,10 +97,13 @@ The Java analyzers can also be used for variants like the ### Making SAST analyzers available to all GitLab tiers -All open source (OSS) analyzers are in the process of being reviewed and potentially moved to GitLab Core tier. Progress can be +All open source (OSS) analyzers are in the process of being reviewed and potentially moved to the GitLab Core tier. Progress can be tracked in the corresponding [epic](https://gitlab.com/groups/gitlab-org/-/epics/2098). +Please note that support for [Docker-in-Docker](#enabling-docker-in-docker) +will not be extended to the GitLab Core tier. + #### Summary of features per tier Different features are available in different [GitLab tiers](https://about.gitlab.com/pricing/), diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md index a614d0a61d4..cb9f0491b44 100644 --- a/doc/user/project/settings/import_export.md +++ b/doc/user/project/settings/import_export.md @@ -97,7 +97,7 @@ The following items will be exported: - Project and wiki repositories - Project uploads -- Project configuration, including services +- Project configuration, excluding integrations - Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, time tracking, and other project entities - Design Management files and data diff --git a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml index 97ee0c358ab..096f6a786db 100644 --- a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml @@ -90,7 +90,6 @@ eslint-sast: - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false' when: never - if: $CI_COMMIT_BRANCH && - $GITLAB_FEATURES =~ /\bsast\b/ && $SAST_DEFAULT_ANALYZERS =~ /eslint/ exists: - '**/*.html' diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml index 7d2a26b2d39..8bffb2aa192 100644 --- a/lib/gitlab/import_export/project/import_export.yml +++ b/lib/gitlab/import_export/project/import_export.yml @@ -89,7 +89,6 @@ tree: - :triggers - :pipeline_schedules - :container_expiration_policy - - :services - protected_branches: - :merge_access_levels - :push_access_levels @@ -261,12 +260,6 @@ excluded_attributes: runners: - :token - :token_encrypted - services: - - :description - - :inherit_from_id - - :instance - - :template - - :title error_tracking_setting: - :encrypted_token - :encrypted_token_iv @@ -355,8 +348,6 @@ methods: - :type statuses: - :type - services: - - :type merge_request_diff_files: - :utf8_diff merge_requests: diff --git a/lib/gitlab/import_export/project/relation_factory.rb b/lib/gitlab/import_export/project/relation_factory.rb index 3ab9f2c4bfa..ae92228276e 100644 --- a/lib/gitlab/import_export/project/relation_factory.rb +++ b/lib/gitlab/import_export/project/relation_factory.rb @@ -70,10 +70,8 @@ module Gitlab private def invalid_relation? - # Do not create relation if it is: - # - An unknown service - # - A legacy trigger - unknown_service? || legacy_trigger? + # Do not create relation if it is a legacy trigger + legacy_trigger? end def setup_models @@ -137,11 +135,6 @@ module Gitlab end end - def unknown_service? - @relation_name == :services && parsed_relation_hash['type'] && - !Object.const_defined?(parsed_relation_hash['type']) - end - def legacy_trigger? @relation_name == :'Ci::Trigger' && @relation_hash['owner_id'].nil? end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 65e092a9f12..ddd3429c495 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -824,6 +824,9 @@ msgstr "" msgid "- show less" msgstr "" +msgid "0 bytes" +msgstr "" + msgid "0 for unlimited" msgstr "" @@ -1124,6 +1127,9 @@ msgstr "" msgid "ACTION REQUIRED: Something went wrong while obtaining the Let's Encrypt certificate for GitLab Pages domain '%{domain}'" msgstr "" +msgid "API Help" +msgstr "" + msgid "API Token" msgstr "" @@ -1594,6 +1600,9 @@ msgstr "" msgid "Admin Overview" msgstr "" +msgid "Admin Section" +msgstr "" + msgid "Admin mode already enabled" msgstr "" @@ -6300,6 +6309,9 @@ msgstr "" msgid "ContainerRegistry|Image tags" msgstr "" +msgid "ContainerRegistry|Invalid tag: missing manifest digest" +msgstr "" + msgid "ContainerRegistry|Login" msgstr "" @@ -14014,6 +14026,9 @@ msgstr "" msgid "Markdown" msgstr "" +msgid "Markdown Help" +msgstr "" + msgid "Markdown enabled" msgstr "" @@ -15101,6 +15116,9 @@ msgstr "" msgid "My-Reaction" msgstr "" +msgid "N/A" +msgstr "" + msgid "Name" msgstr "" @@ -16605,6 +16623,9 @@ msgstr "" msgid "Permissions" msgstr "" +msgid "Permissions Help" +msgstr "" + msgid "Permissions, LFS, 2FA" msgstr "" @@ -17748,7 +17769,7 @@ msgstr "" msgid "Project clone URL" msgstr "" -msgid "Project configuration, including services" +msgid "Project configuration, excluding integrations" msgstr "" msgid "Project description (optional)" @@ -18777,6 +18798,9 @@ msgstr "" msgid "Public - The project can be accessed without any authentication." msgstr "" +msgid "Public Access Help" +msgstr "" + msgid "Public deploy keys (%{deploy_keys_count})" msgstr "" @@ -18909,6 +18933,9 @@ msgstr "" msgid "README" msgstr "" +msgid "Rake Tasks Help" +msgstr "" + msgid "Raw blob request rate limit per minute" msgstr "" @@ -20016,6 +20043,9 @@ msgstr "" msgid "SSH Keys" msgstr "" +msgid "SSH Keys Help" +msgstr "" + msgid "SSH host key fingerprints" msgstr "" @@ -20145,6 +20175,9 @@ msgstr "" msgid "Search Button" msgstr "" +msgid "Search Jira issues" +msgstr "" + msgid "Search Milestones" msgstr "" @@ -22575,6 +22608,9 @@ msgstr "" msgid "System Hooks" msgstr "" +msgid "System Hooks Help" +msgstr "" + msgid "System Info" msgstr "" @@ -25334,6 +25370,9 @@ msgstr "" msgid "User restrictions" msgstr "" +msgid "User settings" +msgstr "" + msgid "User was successfully created." msgstr "" @@ -26043,6 +26082,9 @@ msgstr "" msgid "Webhooks" msgstr "" +msgid "Webhooks Help" +msgstr "" + msgid "Webhooks allow you to trigger a URL if, for example, new code is pushed or a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. Group webhooks will apply to all projects in a group, allowing you to standardize webhook functionality across your entire group." msgstr "" @@ -26315,6 +26357,9 @@ msgstr "" msgid "Work in progress Limit" msgstr "" +msgid "Workflow Help" +msgstr "" + msgid "Write" msgstr "" diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb index bae6bd07b67..0849fb00e73 100644 --- a/spec/controllers/search_controller_spec.rb +++ b/spec/controllers/search_controller_spec.rb @@ -211,4 +211,9 @@ RSpec.describe SearchController do end.to raise_error(ActionController::ParameterMissing) end end + + describe 'GET #autocomplete' do + it_behaves_like 'when the user cannot read cross project', :autocomplete, { term: 'hello' } + it_behaves_like 'with external authorization service enabled', :autocomplete, { term: 'hello' } + end end diff --git a/spec/features/issuables/issuable_list_spec.rb b/spec/features/issuables/issuable_list_spec.rb index 8cf4dac9afe..259c09b9d11 100644 --- a/spec/features/issuables/issuable_list_spec.rb +++ b/spec/features/issuables/issuable_list_spec.rb @@ -30,7 +30,7 @@ RSpec.describe 'issuable list', :js do expect(first('.issuable-upvotes')).to have_content(1) expect(first('.issuable-downvotes')).to have_content(1) - expect(first('.fa-comments').find(:xpath, '..')).to have_content(2) + expect(first('.issuable-comments')).to have_content(2) end it 'sorts labels alphabetically' do diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz Binary files differindex 730e586b278..b93da033aea 100644 --- a/spec/features/projects/import_export/test_project_export.tar.gz +++ b/spec/features/projects/import_export/test_project_export.tar.gz diff --git a/spec/features/signed_commits_spec.rb b/spec/features/signed_commits_spec.rb index 04ca8a09ca8..d679e4dbb99 100644 --- a/spec/features/signed_commits_spec.rb +++ b/spec/features/signed_commits_spec.rb @@ -72,6 +72,7 @@ RSpec.describe 'GPG signed commits' do it 'unverified signature' do visit project_commit_path(project, GpgHelpers::SIGNED_COMMIT_SHA) + wait_for_all_requests page.find('.gpg-status-box', text: 'Unverified').click @@ -85,6 +86,7 @@ RSpec.describe 'GPG signed commits' do user_2_key visit project_commit_path(project, GpgHelpers::DIFFERING_EMAIL_SHA) + wait_for_all_requests page.find('.gpg-status-box', text: 'Unverified').click @@ -100,6 +102,7 @@ RSpec.describe 'GPG signed commits' do user_2_key visit project_commit_path(project, GpgHelpers::SIGNED_COMMIT_SHA) + wait_for_all_requests page.find('.gpg-status-box', text: 'Unverified').click @@ -115,6 +118,7 @@ RSpec.describe 'GPG signed commits' do user_1_key visit project_commit_path(project, GpgHelpers::SIGNED_AND_AUTHORED_SHA) + wait_for_all_requests page.find('.gpg-status-box', text: 'Verified').click @@ -130,6 +134,7 @@ RSpec.describe 'GPG signed commits' do user_1_key visit project_commit_path(project, GpgHelpers::SIGNED_AND_AUTHORED_SHA) + wait_for_all_requests # wait for the signature to get generated expect(page).to have_selector('.gpg-status-box', text: 'Verified') @@ -137,6 +142,7 @@ RSpec.describe 'GPG signed commits' do user_1.destroy! refresh + wait_for_all_requests page.find('.gpg-status-box', text: 'Verified').click @@ -153,6 +159,7 @@ RSpec.describe 'GPG signed commits' do shared_examples 'a commit with a signature' do before do visit project_tree_path(project, 'signed-commits') + wait_for_all_requests end it 'displays commit signature' do diff --git a/spec/fixtures/gitlab/import_export/corrupted_project_export.tar.gz b/spec/fixtures/gitlab/import_export/corrupted_project_export.tar.gz Binary files differindex e0830c290d1..e99136e96b7 100644 --- a/spec/fixtures/gitlab/import_export/corrupted_project_export.tar.gz +++ b/spec/fixtures/gitlab/import_export/corrupted_project_export.tar.gz diff --git a/spec/fixtures/gitlab/import_export/lightweight_project_export.tar.gz b/spec/fixtures/gitlab/import_export/lightweight_project_export.tar.gz Binary files differindex 0aa41734778..e3ec4f603b9 100644 --- a/spec/fixtures/gitlab/import_export/lightweight_project_export.tar.gz +++ b/spec/fixtures/gitlab/import_export/lightweight_project_export.tar.gz diff --git a/spec/fixtures/lib/gitlab/import_export/complex/project.json b/spec/fixtures/lib/gitlab/import_export/complex/project.json index f6a6671b7f1..d88b2ebc83a 100644 --- a/spec/fixtures/lib/gitlab/import_export/complex/project.json +++ b/spec/fixtures/lib/gitlab/import_export/complex/project.json @@ -7007,376 +7007,6 @@ "enabled": false }, "deploy_keys": [], - "services": [ - { - "id": 101, - "project_id": 5, - "created_at": "2016-06-14T15:01:51.327Z", - "updated_at": "2016-06-14T15:01:51.327Z", - "active": false, - "properties": {}, - "template": false, - "push_events": true, - "issues_events": true, - "merge_requests_events": true, - "tag_push_events": true, - "note_events": true, - "job_events": true, - "type": "YoutrackService", - "category": "issue_tracker", - "default": false, - "wiki_page_events": true - }, - { - "id": 100, - "project_id": 5, - "created_at": "2016-06-14T15:01:51.315Z", - "updated_at": "2016-06-14T15:01:51.315Z", - "active": false, - "properties": {}, - "template": false, - "push_events": true, - "issues_events": true, - "merge_requests_events": true, - "tag_push_events": true, - "note_events": true, - "job_events": true, - "type": "TeamcityService", - "category": "ci", - "default": false, - "wiki_page_events": true - }, - { - "id": 99, - "project_id": 5, - "created_at": "2016-06-14T15:01:51.303Z", - "updated_at": "2016-06-14T15:01:51.303Z", - "active": false, - "properties": { - "notify_only_broken_pipelines": true - }, - "template": false, - "push_events": true, - "issues_events": true, - "merge_requests_events": true, - "tag_push_events": true, - "note_events": true, - "pipeline_events": true, - "type": "SlackService", - "category": "common", - "default": false, - "wiki_page_events": true - }, - { - "id": 98, - "project_id": 5, - "created_at": "2016-06-14T15:01:51.289Z", - "updated_at": "2016-06-14T15:01:51.289Z", - "active": false, - "properties": {}, - "template": false, - "push_events": true, - "issues_events": true, - "merge_requests_events": true, - "tag_push_events": true, - "note_events": true, - "job_events": true, - "type": "RedmineService", - "category": "issue_tracker", - "default": false, - "wiki_page_events": true - }, - { - "id": 97, - "project_id": 5, - "created_at": "2016-06-14T15:01:51.277Z", - "updated_at": "2016-06-14T15:01:51.277Z", - "active": false, - "properties": {}, - "template": false, - "push_events": true, - "issues_events": true, - "merge_requests_events": true, - "tag_push_events": true, - "note_events": true, - "job_events": true, - "type": "PushoverService", - "category": "common", - "default": false, - "wiki_page_events": true - }, - { - "id": 96, - "project_id": 5, - "created_at": "2016-06-14T15:01:51.267Z", - "updated_at": "2016-06-14T15:01:51.267Z", - "active": false, - "properties": {}, - "template": false, - "push_events": true, - "issues_events": true, - "merge_requests_events": true, - "tag_push_events": true, - "note_events": true, - "job_events": true, - "type": "PivotalTrackerService", - "category": "common", - "default": false, - "wiki_page_events": true - }, - { - "id": 95, - "project_id": 5, - "created_at": "2016-06-14T15:01:51.255Z", - "updated_at": "2016-06-14T15:01:51.255Z", - "active": false, - "properties": { - "api_url": "", - "jira_issue_transition_id": "2" - }, - "template": false, - "push_events": true, - "issues_events": true, - "merge_requests_events": true, - "tag_push_events": true, - "note_events": true, - "job_events": true, - "type": "JiraService", - "category": "issue_tracker", - "default": false, - "wiki_page_events": true - }, - { - "id": 94, - "project_id": 5, - "created_at": "2016-06-14T15:01:51.232Z", - "updated_at": "2016-06-14T15:01:51.232Z", - "active": true, - "properties": {}, - "template": false, - "push_events": true, - "issues_events": true, - "merge_requests_events": true, - "tag_push_events": true, - "note_events": true, - "job_events": true, - "type": "IrkerService", - "category": "common", - "default": false, - "wiki_page_events": true - }, - { - "id": 93, - "project_id": 5, - "created_at": "2016-06-14T15:01:51.219Z", - "updated_at": "2016-06-14T15:01:51.219Z", - "active": false, - "properties": { - "notify_only_broken_pipelines": true - }, - "template": false, - "push_events": true, - "issues_events": true, - "merge_requests_events": true, - "tag_push_events": true, - "note_events": true, - "pipeline_events": true, - "type": "HipchatService", - "category": "common", - "default": false, - "wiki_page_events": true - }, - { - "id": 91, - "project_id": 5, - "created_at": "2016-06-14T15:01:51.182Z", - "updated_at": "2016-06-14T15:01:51.182Z", - "active": false, - "properties": {}, - "template": false, - "push_events": true, - "issues_events": true, - "merge_requests_events": true, - "tag_push_events": true, - "note_events": true, - "job_events": true, - "type": "FlowdockService", - "category": "common", - "default": false, - "wiki_page_events": true - }, - { - "id": 90, - "project_id": 5, - "created_at": "2016-06-14T15:01:51.166Z", - "updated_at": "2016-06-14T15:01:51.166Z", - "active": false, - "properties": {}, - "template": false, - "push_events": true, - "issues_events": true, - "merge_requests_events": true, - "tag_push_events": true, - "note_events": true, - "job_events": true, - "type": "ExternalWikiService", - "category": "common", - "default": false, - "wiki_page_events": true - }, - { - "id": 89, - "project_id": 5, - "created_at": "2016-06-14T15:01:51.153Z", - "updated_at": "2016-06-14T15:01:51.153Z", - "active": false, - "properties": {}, - "template": false, - "push_events": true, - "issues_events": true, - "merge_requests_events": true, - "tag_push_events": true, - "note_events": true, - "job_events": true, - "type": "EmailsOnPushService", - "category": "common", - "default": false, - "wiki_page_events": true - }, - { - "id": 88, - "project_id": 5, - "created_at": "2016-06-14T15:01:51.139Z", - "updated_at": "2016-06-14T15:01:51.139Z", - "active": false, - "properties": {}, - "template": false, - "push_events": true, - "issues_events": true, - "merge_requests_events": true, - "tag_push_events": true, - "note_events": true, - "job_events": true, - "type": "DroneCiService", - "category": "ci", - "default": false, - "wiki_page_events": true - }, - { - "id": 87, - "project_id": 5, - "created_at": "2016-06-14T15:01:51.125Z", - "updated_at": "2016-06-14T15:01:51.125Z", - "active": false, - "properties": {}, - "template": false, - "push_events": true, - "issues_events": true, - "merge_requests_events": true, - "tag_push_events": true, - "note_events": true, - "job_events": true, - "type": "CustomIssueTrackerService", - "category": "issue_tracker", - "default": false, - "wiki_page_events": true - }, - { - "id": 86, - "project_id": 5, - "created_at": "2016-06-14T15:01:51.113Z", - "updated_at": "2016-06-14T15:01:51.113Z", - "active": false, - "properties": {}, - "template": false, - "push_events": true, - "issues_events": true, - "merge_requests_events": true, - "tag_push_events": true, - "note_events": true, - "job_events": true, - "type": "CampfireService", - "category": "common", - "default": false, - "wiki_page_events": true - }, - { - "id": 84, - "project_id": 5, - "created_at": "2016-06-14T15:01:51.080Z", - "updated_at": "2016-06-14T15:01:51.080Z", - "active": false, - "properties": {}, - "template": false, - "push_events": true, - "issues_events": true, - "merge_requests_events": true, - "tag_push_events": true, - "note_events": true, - "job_events": true, - "type": "BuildkiteService", - "category": "ci", - "default": false, - "wiki_page_events": true - }, - { - "id": 83, - "project_id": 5, - "created_at": "2016-06-14T15:01:51.067Z", - "updated_at": "2016-06-14T15:01:51.067Z", - "active": false, - "properties": {}, - "template": false, - "push_events": true, - "issues_events": true, - "merge_requests_events": true, - "tag_push_events": true, - "note_events": true, - "job_events": true, - "type": "BambooService", - "category": "ci", - "default": false, - "wiki_page_events": true - }, - { - "id": 82, - "project_id": 5, - "created_at": "2016-06-14T15:01:51.047Z", - "updated_at": "2016-06-14T15:01:51.047Z", - "active": false, - "properties": {}, - "template": false, - "push_events": true, - "issues_events": true, - "merge_requests_events": true, - "tag_push_events": true, - "note_events": true, - "job_events": true, - "type": "AssemblaService", - "category": "common", - "default": false, - "wiki_page_events": true - }, - { - "id": 81, - "project_id": 5, - "created_at": "2016-06-14T15:01:51.031Z", - "updated_at": "2016-06-14T15:01:51.031Z", - "active": false, - "properties": {}, - "template": false, - "push_events": true, - "issues_events": true, - "merge_requests_events": true, - "tag_push_events": true, - "note_events": true, - "job_events": true, - "type": "AsanaService", - "category": "common", - "default": false, - "wiki_page_events": true - } - ], "hooks": [], "protected_branches": [ { diff --git a/spec/fixtures/lib/gitlab/import_export/designs/project.json b/spec/fixtures/lib/gitlab/import_export/designs/project.json index 28eaa38d387..ebc08868d9e 100644 --- a/spec/fixtures/lib/gitlab/import_export/designs/project.json +++ b/spec/fixtures/lib/gitlab/import_export/designs/project.json @@ -456,9 +456,6 @@ "pipeline_schedules":[ ], - "services":[ - - ], "protected_branches":[ ], diff --git a/spec/fixtures/lib/gitlab/import_export/light/project.json b/spec/fixtures/lib/gitlab/import_export/light/project.json index cef78316361..963cdb342b5 100644 --- a/spec/fixtures/lib/gitlab/import_export/light/project.json +++ b/spec/fixtures/lib/gitlab/import_export/light/project.json @@ -141,48 +141,6 @@ ] } ], - "services": [ - { - "id": 100, - "project_id": 5, - "created_at": "2016-06-14T15:01:51.315Z", - "updated_at": "2016-06-14T15:01:51.315Z", - "active": false, - "properties": {}, - "template": true, - "instance": false, - "push_events": true, - "issues_events": true, - "merge_requests_events": true, - "tag_push_events": true, - "note_events": true, - "job_events": true, - "type": "TeamcityService", - "category": "ci", - "default": false, - "wiki_page_events": true - }, - { - "id": 101, - "project_id": 5, - "created_at": "2016-06-14T15:01:51.315Z", - "updated_at": "2016-06-14T15:01:51.315Z", - "active": false, - "properties": {}, - "template": false, - "instance": true, - "push_events": true, - "issues_events": true, - "merge_requests_events": true, - "tag_push_events": true, - "note_events": true, - "job_events": true, - "type": "JiraService", - "category": "ci", - "default": false, - "wiki_page_events": true - } - ], "snippets": [], "hooks": [], "custom_attributes": [ diff --git a/spec/fixtures/lib/gitlab/import_export/with_invalid_records/project.json b/spec/fixtures/lib/gitlab/import_export/with_invalid_records/project.json index a6e6ba43bdc..b9e791ee85a 100644 --- a/spec/fixtures/lib/gitlab/import_export/with_invalid_records/project.json +++ b/spec/fixtures/lib/gitlab/import_export/with_invalid_records/project.json @@ -32,7 +32,6 @@ ], "labels": [], "issues": [], - "services": [], "snippets": [], "hooks": [] } diff --git a/spec/frontend/blob/components/blob_header_default_actions_spec.js b/spec/frontend/blob/components/blob_header_default_actions_spec.js index 0247a12d8d3..529e7cc85f5 100644 --- a/spec/frontend/blob/components/blob_header_default_actions_spec.js +++ b/spec/frontend/blob/components/blob_header_default_actions_spec.js @@ -13,7 +13,6 @@ describe('Blob Header Default Actions', () => { let wrapper; let btnGroup; let buttons; - const hrefPrefix = 'http://localhost'; function createComponent(propsData = {}) { wrapper = mount(BlobHeaderActions, { @@ -47,11 +46,11 @@ describe('Blob Header Default Actions', () => { }); it('correct href attribute on RAW button', () => { - expect(buttons.at(1).vm.$el.href).toBe(`${hrefPrefix}${Blob.rawPath}`); + expect(buttons.at(1).attributes('href')).toBe(Blob.rawPath); }); it('correct href attribute on Download button', () => { - expect(buttons.at(2).vm.$el.href).toBe(`${hrefPrefix}${Blob.rawPath}?inline=false`); + expect(buttons.at(2).attributes('href')).toBe(`${Blob.rawPath}?inline=false`); }); it('does not render "Copy file contents" button as disables if the viewer is Simple', () => { diff --git a/spec/frontend/environment.js b/spec/frontend/environment.js index 08da34aa27a..c9d77a34595 100644 --- a/spec/frontend/environment.js +++ b/spec/frontend/environment.js @@ -3,12 +3,14 @@ const path = require('path'); const { ErrorWithStack } = require('jest-util'); const JSDOMEnvironment = require('jest-environment-jsdom-sixteen'); +const { TEST_HOST } = require('./helpers/test_constants'); const ROOT_PATH = path.resolve(__dirname, '../..'); class CustomEnvironment extends JSDOMEnvironment { constructor(config, context) { - super(config, context); + // Setup testURL so that window.location is setup properly + super({ ...config, testURL: TEST_HOST }, context); Object.assign(context.console, { error(...args) { @@ -57,6 +59,9 @@ class CustomEnvironment extends JSDOMEnvironment { ownerDocument: this.global.document, }, }); + + // Expose the jsdom (created in super class) to the global so that we can call reconfigure({ url: '' }) to properly set `window.location` + this.global.dom = this.dom; } async teardown() { diff --git a/spec/frontend/fixtures/static/global_search_input.html b/spec/frontend/fixtures/static/search_autocomplete.html index 29db9020424..29db9020424 100644 --- a/spec/frontend/fixtures/static/global_search_input.html +++ b/spec/frontend/fixtures/static/search_autocomplete.html diff --git a/spec/frontend/helpers/test_constants.js b/spec/frontend/helpers/test_constants.js index c97d47a6406..69b78f556aa 100644 --- a/spec/frontend/helpers/test_constants.js +++ b/spec/frontend/helpers/test_constants.js @@ -1,7 +1,19 @@ -export const FIXTURES_PATH = `/fixtures`; -export const TEST_HOST = 'http://test.host'; +const FIXTURES_PATH = `/fixtures`; +const TEST_HOST = 'http://test.host'; -export const DUMMY_IMAGE_URL = `${FIXTURES_PATH}/static/images/one_white_pixel.png`; +const DUMMY_IMAGE_URL = `${FIXTURES_PATH}/static/images/one_white_pixel.png`; -export const GREEN_BOX_IMAGE_URL = `${FIXTURES_PATH}/static/images/green_box.png`; -export const RED_BOX_IMAGE_URL = `${FIXTURES_PATH}/static/images/red_box.png`; +const GREEN_BOX_IMAGE_URL = `${FIXTURES_PATH}/static/images/green_box.png`; +const RED_BOX_IMAGE_URL = `${FIXTURES_PATH}/static/images/red_box.png`; + +// NOTE: module.exports is needed so that this file can be used +// by environment.js +// +// eslint-disable-next-line import/no-commonjs +module.exports = { + FIXTURES_PATH, + TEST_HOST, + DUMMY_IMAGE_URL, + GREEN_BOX_IMAGE_URL, + RED_BOX_IMAGE_URL, +}; diff --git a/spec/frontend/issuables_list/components/issuables_list_app_spec.js b/spec/frontend/issuables_list/components/issuables_list_app_spec.js index 9bd4a994f0a..b135de9e6b5 100644 --- a/spec/frontend/issuables_list/components/issuables_list_app_spec.js +++ b/spec/frontend/issuables_list/components/issuables_list_app_spec.js @@ -7,6 +7,7 @@ import { TEST_HOST } from 'helpers/test_constants'; import flash from '~/flash'; import IssuablesListApp from '~/issuables_list/components/issuables_list_app.vue'; import Issuable from '~/issuables_list/components/issuable.vue'; +import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; import issueablesEventBus from '~/issuables_list/eventhub'; import { PAGE_SIZE, PAGE_SIZE_MANUAL, RELATIVE_POSITION } from '~/issuables_list/constants'; @@ -59,6 +60,7 @@ describe('Issuables list component', () => { const findLoading = () => wrapper.find(GlSkeletonLoading); const findIssuables = () => wrapper.findAll(Issuable); + const findFilteredSearchBar = () => wrapper.find(FilteredSearchBar); const findFirstIssuable = () => findIssuables().wrappers[0]; const findEmptyState = () => wrapper.find(GlEmptyState); @@ -75,6 +77,7 @@ describe('Issuables list component', () => { afterEach(() => { wrapper.destroy(); + wrapper = null; mockAxios.restore(); window.location = oldLocation; }); @@ -131,6 +134,7 @@ describe('Issuables list component', () => { }); it('does not call API until mounted', () => { + factory(); expect(apiSpy).not.toHaveBeenCalled(); }); @@ -173,6 +177,12 @@ describe('Issuables list component', () => { expect(wrapper.find(GlPagination).exists()).toBe(true); }); }); + + it('does not render FilteredSearchBar', () => { + factory(); + + expect(findFilteredSearchBar().exists()).toBe(false); + }); }); describe('with bulk editing enabled', () => { @@ -523,4 +533,48 @@ describe('Issuables list component', () => { }); }); }); + + describe('when type is "jira"', () => { + it('renders FilteredSearchBar', () => { + factory({ type: 'jira' }); + + expect(findFilteredSearchBar().exists()).toBe(true); + }); + + describe('initialSortBy', () => { + const query = '?sort=updated_asc'; + + it('sets default value', () => { + factory({ type: 'jira' }); + + expect(findFilteredSearchBar().props('initialSortBy')).toBe('created_desc'); + }); + + it('sets value according to query', () => { + setUrl(query); + + factory({ type: 'jira' }); + + expect(findFilteredSearchBar().props('initialSortBy')).toBe('updated_asc'); + }); + }); + + describe('initialFilterValue', () => { + it('does not set value when no query', () => { + factory({ type: 'jira' }); + + expect(findFilteredSearchBar().props('initialFilterValue')).toEqual([]); + }); + + it('sets value according to query', () => { + const query = '?search=free+text'; + + setUrl(query); + + factory({ type: 'jira' }); + + expect(findFilteredSearchBar().props('initialFilterValue')).toEqual(['free text']); + }); + }); + }); }); diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js index ba1118ef3b7..996e11b2442 100644 --- a/spec/frontend/monitoring/components/dashboard_spec.js +++ b/spec/frontend/monitoring/components/dashboard_spec.js @@ -36,6 +36,7 @@ import { dashboardProps, } from '../fixture_data'; import createFlash from '~/flash'; +import { TEST_HOST } from 'helpers/test_constants'; jest.mock('~/flash'); @@ -448,7 +449,7 @@ describe('Dashboard', () => { path: 'dashboard©.yml', }); expect(window.location.assign).toHaveBeenCalledWith( - 'http://localhost/?dashboard=dashboard%2526copy.yml', + `${TEST_HOST}/?dashboard=dashboard%2526copy.yml`, ); }); }); diff --git a/spec/frontend/monitoring/utils_spec.js b/spec/frontend/monitoring/utils_spec.js index 1b4df286868..452fd756050 100644 --- a/spec/frontend/monitoring/utils_spec.js +++ b/spec/frontend/monitoring/utils_spec.js @@ -460,7 +460,7 @@ describe('monitoring/utils', () => { expect(urlUtils.updateHistory).toHaveBeenCalledTimes(1); expect(urlUtils.updateHistory).toHaveBeenCalledWith({ - url: `http://localhost/${urlParams}`, + url: `${TEST_HOST}/${urlParams}`, title: '', }); }, diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js index ba084d01742..a4d8927e89b 100644 --- a/spec/frontend/notes/stores/actions_spec.js +++ b/spec/frontend/notes/stores/actions_spec.js @@ -274,9 +274,54 @@ describe('Actions Notes Store', () => { }); }); + describe('fetchData', () => { + describe('given there are no notes', () => { + const lastFetchedAt = '13579'; + + beforeEach(() => { + axiosMock + .onGet(notesDataMock.notesPath) + .replyOnce(200, { notes: [], last_fetched_at: lastFetchedAt }); + }); + + it('should commit SET_LAST_FETCHED_AT', () => + testAction( + actions.fetchData, + undefined, + { notesData: notesDataMock }, + [{ type: 'SET_LAST_FETCHED_AT', payload: lastFetchedAt }], + [], + )); + }); + + describe('given there are notes', () => { + const lastFetchedAt = '12358'; + + beforeEach(() => { + axiosMock + .onGet(notesDataMock.notesPath) + .replyOnce(200, { notes: discussionMock.notes, last_fetched_at: lastFetchedAt }); + }); + + it('should dispatch updateOrCreateNotes, startTaskList and commit SET_LAST_FETCHED_AT', () => + testAction( + actions.fetchData, + undefined, + { notesData: notesDataMock }, + [{ type: 'SET_LAST_FETCHED_AT', payload: lastFetchedAt }], + [ + { type: 'updateOrCreateNotes', payload: discussionMock.notes }, + { type: 'startTaskList' }, + ], + )); + }); + }); + describe('poll', () => { beforeEach(done => { - jest.spyOn(axios, 'get'); + axiosMock + .onGet(notesDataMock.notesPath) + .reply(200, { notes: [], last_fetched_at: '123456' }, { 'poll-interval': '1000' }); store .dispatch('setNotesData', notesDataMock) @@ -285,15 +330,10 @@ describe('Actions Notes Store', () => { }); it('calls service with last fetched state', done => { - axiosMock - .onAny() - .reply(200, { notes: [], last_fetched_at: '123456' }, { 'poll-interval': '1000' }); - store .dispatch('poll') .then(() => new Promise(resolve => requestAnimationFrame(resolve))) .then(() => { - expect(axios.get).toHaveBeenCalled(); expect(store.state.lastFetchedAt).toBe('123456'); jest.advanceTimersByTime(1500); @@ -305,8 +345,9 @@ describe('Actions Notes Store', () => { }), ) .then(() => { - expect(axios.get.mock.calls.length).toBe(2); - expect(axios.get.mock.calls[axios.get.mock.calls.length - 1][1].headers).toEqual({ + const expectedGetRequests = 2; + expect(axiosMock.history.get.length).toBe(expectedGetRequests); + expect(axiosMock.history.get[expectedGetRequests - 1].headers).toMatchObject({ 'X-Last-Fetched-At': '123456', }); }) diff --git a/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js b/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js index 5d465217d78..9e876d6d8a3 100644 --- a/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js +++ b/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js @@ -1,5 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import { GlFormCheckbox, GlSprintf } from '@gitlab/ui'; +import { GlFormCheckbox, GlSprintf, GlIcon } from '@gitlab/ui'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; @@ -9,6 +9,9 @@ import DetailsRow from '~/registry/explorer/components/details_page/details_row. import { REMOVE_TAG_BUTTON_TITLE, REMOVE_TAG_BUTTON_DISABLE_TOOLTIP, + MISSING_MANIFEST_WARNING_TOOLTIP, + NOT_AVAILABLE_TEXT, + NOT_AVAILABLE_SIZE, } from '~/registry/explorer/constants/index'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; @@ -33,6 +36,7 @@ describe('tags list row', () => { const findPublishedDateDetail = () => wrapper.find('[data-testid="published-date-detail"]'); const findManifestDetail = () => wrapper.find('[data-testid="manifest-detail"]'); const findConfigurationDetail = () => wrapper.find('[data-testid="configuration-detail"]'); + const findWarningIcon = () => wrapper.find(GlIcon); const mountComponent = (propsData = defaultProps) => { wrapper = shallowMount(component, { @@ -68,6 +72,11 @@ describe('tags list row', () => { expect(findCheckbox().exists()).toBe(false); }); + it('is disabled when the digest is missing', () => { + mountComponent({ tag: { ...tag, digest: null } }); + expect(findCheckbox().attributes('disabled')).toBe('true'); + }); + it('is wired to the selected prop', () => { mountComponent({ ...defaultProps, selected: true }); @@ -134,6 +143,27 @@ describe('tags list row', () => { }); }); + describe('warning icon', () => { + it('is normally hidden', () => { + mountComponent(); + + expect(findWarningIcon().exists()).toBe(false); + }); + + it('is shown when the tag is broken', () => { + mountComponent({ tag: { ...tag, digest: null } }); + + expect(findWarningIcon().exists()).toBe(true); + }); + + it('has an appropriate tooltip', () => { + mountComponent({ tag: { ...tag, digest: null } }); + + const tooltip = getBinding(findWarningIcon().element, 'gl-tooltip'); + expect(tooltip.value.title).toBe(MISSING_MANIFEST_WARNING_TOOLTIP); + }); + }); + describe('size', () => { it('exists', () => { mountComponent(); @@ -150,7 +180,7 @@ describe('tags list row', () => { it('when total_size is missing', () => { mountComponent(); - expect(findSize().text()).toMatchInterpolatedText('10 layers'); + expect(findSize().text()).toMatchInterpolatedText(`${NOT_AVAILABLE_SIZE} · 10 layers`); }); it('when layers are missing', () => { @@ -162,7 +192,7 @@ describe('tags list row', () => { it('when there is 1 layer', () => { mountComponent({ ...defaultProps, tag: { ...tag, layers: 1 } }); - expect(findSize().text()).toMatchInterpolatedText('1 layer'); + expect(findSize().text()).toMatchInterpolatedText(`${NOT_AVAILABLE_SIZE} · 1 layer`); }); }); @@ -204,6 +234,12 @@ describe('tags list row', () => { expect(findShortRevision().text()).toMatchInterpolatedText('Digest: 1ab51d5'); }); + + it(`displays ${NOT_AVAILABLE_TEXT} when digest is missing`, () => { + mountComponent({ tag: { ...tag, digest: null } }); + + expect(findShortRevision().text()).toMatchInterpolatedText(`Digest: ${NOT_AVAILABLE_TEXT}`); + }); }); describe('delete button', () => { @@ -223,11 +259,19 @@ describe('tags list row', () => { }); }); - it('is disabled when tag has no destroy path', () => { - mountComponent({ ...defaultProps, tag: { ...tag, destroy_path: null } }); - - expect(findDeleteButton().attributes('disabled')).toBe('true'); - }); + it.each` + destroy_path | digest + ${'foo'} | ${null} + ${null} | ${'foo'} + ${null} | ${null} + `( + 'is disabled when destroy_path is $destroy_path and digest is $digest', + ({ destroy_path, digest }) => { + mountComponent({ ...defaultProps, tag: { ...tag, destroy_path, digest } }); + + expect(findDeleteButton().attributes('disabled')).toBe('true'); + }, + ); it('delete event emits delete', () => { mountComponent(); @@ -239,36 +283,47 @@ describe('tags list row', () => { }); describe('details rows', () => { - beforeEach(() => { - mountComponent(); + describe('when the tag has a digest', () => { + beforeEach(() => { + mountComponent(); - return wrapper.vm.$nextTick(); - }); - - it('has 3 details rows', () => { - expect(findDetailsRows().length).toBe(3); - }); + return wrapper.vm.$nextTick(); + }); - describe.each` - name | finderFunction | text | icon | clipboard - ${'published date detail'} | ${findPublishedDateDetail} | ${'Published to the bar image repository at 10:23 GMT+0000 on 2020-06-29'} | ${'clock'} | ${false} - ${'manifest detail'} | ${findManifestDetail} | ${'Manifest digest: sha256:1ab51d519f574b636ae7788051c60239334ae8622a9fd82a0cf7bae7786dfd5c'} | ${'log'} | ${true} - ${'configuration detail'} | ${findConfigurationDetail} | ${'Configuration digest: sha256:b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43'} | ${'cloud-gear'} | ${true} - `('$name details row', ({ finderFunction, text, icon, clipboard }) => { - it(`has ${text} as text`, () => { - expect(finderFunction().text()).toMatchInterpolatedText(text); + it('has 3 details rows', () => { + expect(findDetailsRows().length).toBe(3); }); - it(`has the ${icon} icon`, () => { - expect(finderFunction().props('icon')).toBe(icon); + describe.each` + name | finderFunction | text | icon | clipboard + ${'published date detail'} | ${findPublishedDateDetail} | ${'Published to the bar image repository at 10:23 GMT+0000 on 2020-06-29'} | ${'clock'} | ${false} + ${'manifest detail'} | ${findManifestDetail} | ${'Manifest digest: sha256:1ab51d519f574b636ae7788051c60239334ae8622a9fd82a0cf7bae7786dfd5c'} | ${'log'} | ${true} + ${'configuration detail'} | ${findConfigurationDetail} | ${'Configuration digest: sha256:b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43'} | ${'cloud-gear'} | ${true} + `('$name details row', ({ finderFunction, text, icon, clipboard }) => { + it(`has ${text} as text`, () => { + expect(finderFunction().text()).toMatchInterpolatedText(text); + }); + + it(`has the ${icon} icon`, () => { + expect(finderFunction().props('icon')).toBe(icon); + }); + + it(`is ${clipboard} that clipboard button exist`, () => { + expect( + finderFunction() + .find(ClipboardButton) + .exists(), + ).toBe(clipboard); + }); }); + }); + + describe('when the tag does not have a digest', () => { + it('hides the details rows', async () => { + mountComponent({ tag: { ...tag, digest: null } }); - it(`is ${clipboard} that clipboard button exist`, () => { - expect( - finderFunction() - .find(ClipboardButton) - .exists(), - ).toBe(clipboard); + await wrapper.vm.$nextTick(); + expect(findDetailsRows().length).toBe(0); }); }); }); diff --git a/spec/frontend/repository/utils/dom_spec.js b/spec/frontend/repository/utils/dom_spec.js index 0b61161c9d0..e8b0565868e 100644 --- a/spec/frontend/repository/utils/dom_spec.js +++ b/spec/frontend/repository/utils/dom_spec.js @@ -1,5 +1,6 @@ import { setHTMLFixture } from '../../helpers/fixtures'; import { updateElementsVisibility, updateFormAction } from '~/repository/utils/dom'; +import { TEST_HOST } from 'helpers/test_constants'; describe('updateElementsVisibility', () => { it('adds hidden class', () => { @@ -31,7 +32,7 @@ describe('updateFormAction', () => { updateFormAction('.js-test', '/gitlab/create', path); expect(document.querySelector('.js-test').action).toBe( - `http://localhost/gitlab/create/${path.replace(/^\//, '')}`, + `${TEST_HOST}/gitlab/create/${path.replace(/^\//, '')}`, ); }); }); diff --git a/spec/frontend/global_search_input_spec.js b/spec/frontend/search_autocomplete_spec.js index 8c00ea5f193..1003a076f9e 100644 --- a/spec/frontend/global_search_input_spec.js +++ b/spec/frontend/search_autocomplete_spec.js @@ -2,30 +2,24 @@ import $ from 'jquery'; import '~/gl_dropdown'; -import initGlobalSearchInput from '~/global_search_input'; +import initSearchAutocomplete from '~/search_autocomplete'; import '~/lib/utils/common_utils'; +import axios from '~/lib/utils/axios_utils'; +import AxiosMockAdapter from 'axios-mock-adapter'; -describe('Global search input dropdown', () => { +describe('Search autocomplete dropdown', () => { let widget = null; const userName = 'root'; - const userId = 1; - const dashboardIssuesPath = '/dashboard/issues'; - const dashboardMRsPath = '/dashboard/merge_requests'; - const projectIssuesPath = '/gitlab-org/gitlab-foss/issues'; - const projectMRsPath = '/gitlab-org/gitlab-foss/-/merge_requests'; - const groupIssuesPath = '/groups/gitlab-org/-/issues'; - const groupMRsPath = '/groups/gitlab-org/-/merge_requests'; - + const autocompletePath = '/search/autocomplete'; const projectName = 'GitLab Community Edition'; - const groupName = 'Gitlab Org'; const removeBodyAttributes = () => { @@ -112,15 +106,15 @@ describe('Global search input dropdown', () => { expect(list.find(mrsIHaveCreatedLink).text()).toBe("Merge requests I've created"); }; - preloadFixtures('static/global_search_input.html'); + preloadFixtures('static/search_autocomplete.html'); beforeEach(() => { - loadFixtures('static/global_search_input.html'); + loadFixtures('static/search_autocomplete.html'); window.gon = {}; window.gon.current_user_id = userId; window.gon.current_username = userName; - return (widget = initGlobalSearchInput()); + return (widget = initSearchAutocomplete({ autocompletePath })); }); afterEach(() => { @@ -183,31 +177,105 @@ describe('Global search input dropdown', () => { widget.wrap.trigger($.Event('keydown', { which: DOWN })); const enterKeyEvent = $.Event('keydown', { which: ENTER }); widget.searchInput.trigger(enterKeyEvent); + // This does not currently catch failing behavior. For security reasons, // browsers will not trigger default behavior (form submit, in this // example) on JavaScript-created keypresses. expect(submitSpy).not.toHaveBeenCalled(); }); - describe('disableDropdown', () => { + describe('show autocomplete results', () => { + beforeEach(() => { + widget.enableAutocomplete(); + + const axiosMock = new AxiosMockAdapter(axios); + const autocompleteUrl = new RegExp(autocompletePath); + + axiosMock.onGet(autocompleteUrl).reply(200, [ + { + category: 'Projects', + id: 1, + value: 'Gitlab Test', + label: 'Gitlab Org / Gitlab Test', + url: '/gitlab-org/gitlab-test', + avatar_url: '', + }, + { + category: 'Groups', + id: 1, + value: 'Gitlab Org', + label: 'Gitlab Org', + url: '/gitlab-org', + avatar_url: '', + }, + ]); + }); + + function triggerAutocomplete() { + return new Promise(resolve => { + const dropdown = widget.searchInput.data('glDropdown'); + const filterCallback = dropdown.filter.options.callback; + dropdown.filter.options.callback = jest.fn(data => { + filterCallback(data); + + resolve(); + }); + + widget.searchInput.val('Gitlab'); + widget.searchInput.triggerHandler('input'); + }); + } + + it('suggest Projects', done => { + // eslint-disable-next-line promise/catch-or-return + triggerAutocomplete().finally(() => { + const list = widget.wrap.find('.dropdown-menu').find('ul'); + const link = "a[href$='/gitlab-org/gitlab-test']"; + + expect(list.find(link).length).toBe(1); + + done(); + }); + + // Make sure jest properly acknowledge the `done` invocation + jest.runOnlyPendingTimers(); + }); + + it('suggest Groups', done => { + // eslint-disable-next-line promise/catch-or-return + triggerAutocomplete().finally(() => { + const list = widget.wrap.find('.dropdown-menu').find('ul'); + const link = "a[href$='/gitlab-org']"; + + expect(list.find(link).length).toBe(1); + + done(); + }); + + // Make sure jest properly acknowledge the `done` invocation + jest.runOnlyPendingTimers(); + }); + }); + + describe('disableAutocomplete', () => { beforeEach(() => { - widget.enableDropdown(); + widget.enableAutocomplete(); }); it('should close the Dropdown', () => { const toggleSpy = jest.spyOn(widget.dropdownToggle, 'dropdown'); widget.dropdown.addClass('show'); - widget.disableDropdown(); + widget.disableAutocomplete(); expect(toggleSpy).toHaveBeenCalledWith('toggle'); }); }); - describe('enableDropdown', () => { + describe('enableAutocomplete', () => { it('should open the Dropdown', () => { const toggleSpy = jest.spyOn(widget.dropdownToggle, 'dropdown'); - widget.enableDropdown(); + widget.enableAutocomplete(); expect(toggleSpy).toHaveBeenCalledWith('toggle'); }); diff --git a/spec/frontend/self_monitor/components/self_monitor_form_spec.js b/spec/frontend/self_monitor/components/self_monitor_form_spec.js index 0e6abba08f3..aa6f71b6412 100644 --- a/spec/frontend/self_monitor/components/self_monitor_form_spec.js +++ b/spec/frontend/self_monitor/components/self_monitor_form_spec.js @@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils'; import { GlDeprecatedButton } from '@gitlab/ui'; import SelfMonitor from '~/self_monitor/components/self_monitor_form.vue'; import { createStore } from '~/self_monitor/store'; +import { TEST_HOST } from 'helpers/test_constants'; describe('self monitor component', () => { let wrapper; @@ -82,7 +83,7 @@ describe('self monitor component', () => { .find({ ref: 'selfMonitoringFormText' }) .find('a') .attributes('href'), - ).toEqual('http://localhost/instance-administrators-random/gitlab-self-monitoring'); + ).toEqual(`${TEST_HOST}/instance-administrators-random/gitlab-self-monitoring`); }); }); }); diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb index 1360a4264a9..699232e67b1 100644 --- a/spec/helpers/search_helper_spec.rb +++ b/spec/helpers/search_helper_spec.rb @@ -8,6 +8,99 @@ RSpec.describe SearchHelper do str end + describe 'search_autocomplete_opts' do + context "with no current user" do + before do + allow(self).to receive(:current_user).and_return(nil) + end + + it "returns nil" do + expect(search_autocomplete_opts("q")).to be_nil + end + end + + context "with a standard user" do + let(:user) { create(:user) } + + before do + allow(self).to receive(:current_user).and_return(user) + end + + it "includes Help sections" do + expect(search_autocomplete_opts("hel").size).to eq(9) + end + + it "includes default sections" do + expect(search_autocomplete_opts("dash").size).to eq(1) + end + + it "does not include admin sections" do + expect(search_autocomplete_opts("admin").size).to eq(0) + end + + it "does not allow regular expression in search term" do + expect(search_autocomplete_opts("(webhooks|api)").size).to eq(0) + end + + it "includes the user's groups" do + create(:group).add_owner(user) + expect(search_autocomplete_opts("gro").size).to eq(1) + end + + it "includes nested group" do + create(:group, :nested, name: 'foo').add_owner(user) + expect(search_autocomplete_opts('foo').size).to eq(1) + end + + it "includes the user's projects" do + project = create(:project, namespace: create(:namespace, owner: user)) + expect(search_autocomplete_opts(project.name).size).to eq(1) + end + + it "includes the required project attrs" do + project = create(:project, namespace: create(:namespace, owner: user)) + result = search_autocomplete_opts(project.name).first + + expect(result.keys).to match_array(%i[category id value label url avatar_url]) + end + + it "includes the required group attrs" do + create(:group).add_owner(user) + result = search_autocomplete_opts("gro").first + + expect(result.keys).to match_array(%i[category id label url avatar_url]) + end + + it "does not include the public group" do + group = create(:group) + expect(search_autocomplete_opts(group.name).size).to eq(0) + end + + context "with a current project" do + before do + @project = create(:project, :repository) + end + + it "includes project-specific sections" do + expect(search_autocomplete_opts("Files").size).to eq(1) + expect(search_autocomplete_opts("Commits").size).to eq(1) + end + end + end + + context 'with an admin user' do + let(:admin) { create(:admin) } + + before do + allow(self).to receive(:current_user).and_return(admin) + end + + it "includes admin sections" do + expect(search_autocomplete_opts("admin").size).to eq(1) + end + end + end + describe 'search_entries_info' do using RSpec::Parameterized::TableSyntax diff --git a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb index 8e048d877a1..5b6be0b3198 100644 --- a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb +++ b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb @@ -175,14 +175,6 @@ RSpec.describe Gitlab::ImportExport::FastHashSerializer do expect(subject['merge_requests'].first['resource_label_events']).not_to be_empty end - it 'saves the correct service type' do - expect(subject['services'].first['type']).to eq('CustomIssueTrackerService') - end - - it 'saves the properties for a service' do - expect(subject['services'].first['properties']).to eq('one' => 'value') - end - it 'has project feature' do project_feature = subject['project_feature'] expect(project_feature).not_to be_empty diff --git a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb index e43686e632a..6d5604dc40f 100644 --- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb @@ -291,10 +291,6 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do expect(@project.auto_devops.deploy_strategy).to eq('continuous') end - it 'restores the correct service' do - expect(CustomIssueTrackerService.first).not_to be_nil - end - it 'restores zoom meetings' do meetings = @project.issues.first.zoom_meetings @@ -553,8 +549,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do labels: 2, label_with_priorities: 'A project label', milestones: 1, - first_issue_labels: 1, - services: 1 + first_issue_labels: 1 end context 'when there is an existing build with build token' do @@ -637,7 +632,6 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do label_with_priorities: 'A project label', milestones: 1, first_issue_labels: 1, - services: 1, import_failures: 1 it 'records the failures in the database' do @@ -757,18 +751,6 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do setup_reader(reader) end - it 'does not import any templated services' do - expect(restored_project_json).to eq(true) - - expect(project.services.where(template: true).count).to eq(0) - end - - it 'does not import any instance services' do - expect(restored_project_json).to eq(true) - - expect(project.services.where(instance: true).count).to eq(0) - end - it 'imports labels' do create(:group_label, name: 'Another label', group: project.group) @@ -972,7 +954,6 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do label_with_priorities: nil, milestones: 1, first_issue_labels: 0, - services: 0, import_failures: 1 it 'records the failures in the database' do diff --git a/spec/lib/gitlab/import_export/project/tree_saver_spec.rb b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb index 4f6bc19ebd1..40c103eeda6 100644 --- a/spec/lib/gitlab/import_export/project/tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb @@ -223,18 +223,6 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver do it { is_expected.not_to be_empty } end - context 'with services' do - let(:relation_name) { :services } - - it 'saves the correct service type' do - expect(subject.first['type']).to eq('CustomIssueTrackerService') - end - - it 'saves the properties for a service' do - expect(subject.first['properties']).to eq('one' => 'value') - end - end - context 'with project_feature' do let(:relation_name) { :project_feature } @@ -453,7 +441,6 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver do create(:resource_label_event, label: group_label, merge_request: merge_request) create(:event, :created, target: milestone, project: project, author: user) - create(:service, project: project, type: 'CustomIssueTrackerService', category: 'issue_tracker', properties: { one: 'value' }) create(:project_custom_attribute, project: project) create(:project_custom_attribute, project: project) diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 395eda1754f..9707c0a4ff4 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -461,36 +461,6 @@ DeployKey: - public - can_push - last_used_at -Service: -- id -- type -- title -- project_id -- created_at -- updated_at -- active -- properties -- template -- instance -- alert_events -- push_events -- issues_events -- commit_events -- merge_requests_events -- tag_push_events -- note_events -- pipeline_events -- job_events -- comment_on_event_enabled -- comment_detail -- category -- default -- wiki_page_events -- confidential_issues_events -- confidential_note_events -- deployment_events -- description -- inherit_from_id ProjectHook: - id - url diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 53ef1db6a3e..b8d39f49224 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -512,7 +512,7 @@ RSpec.describe Ci::CreatePipelineService do it 'pull it from Auto-DevOps' do pipeline = execute_service expect(pipeline).to be_auto_devops_source - expect(pipeline.builds.map(&:name)).to match_array(%w[test code_quality build]) + expect(pipeline.builds.map(&:name)).to match_array(%w[build code_quality eslint-sast test]) end end diff --git a/spec/services/jira/requests/issues/list_service_spec.rb b/spec/services/jira/requests/issues/list_service_spec.rb index adfc9771cc7..289dd444225 100644 --- a/spec/services/jira/requests/issues/list_service_spec.rb +++ b/spec/services/jira/requests/issues/list_service_spec.rb @@ -93,6 +93,16 @@ RSpec.describe Jira::Requests::Issues::ListService do subject end end + + context 'without pagination parameters' do + let(:params) { {} } + + it 'uses the default options' do + expect(client).to receive(:get).with(include('startAt=0&maxResults=100')) + + subject + end + end end end end |