Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-07-10 09:09:01 +00:00
parent ff1701e51d
commit cdd71cf36a
61 changed files with 990 additions and 718 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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',
},
},
];

View File

@ -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');

View File

@ -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(() => {

View File

@ -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>

View File

@ -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>

View File

@ -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;

View File

@ -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;
this.highlightFirstRow();
this.setScrollFade();
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,
});
}
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 = [];
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>`;
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;
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);
}

View File

@ -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

View File

@ -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'),

View File

@ -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

View File

@ -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

View File

@ -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 }

View File

@ -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 }

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,5 @@
---
title: Fix comment loading error in issues and merge requests
merge_request: 36043
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Exclude integrations (services) from Project Import/Export
merge_request: 35249
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Replace fa-comment / fa-comments icons with GitLab SVG
merge_request: 36206
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Add broken tag state to tags list items
merge_request: 36442
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Bring SAST to Core - eslint
merge_request: 36392
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Restore the search autocomplete for groups/project/other
merge_request: 35983
author:
type: other

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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/),

View File

@ -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

View File

@ -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'

View File

@ -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:

View File

@ -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

View File

@ -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 ""

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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": [
{

View File

@ -455,9 +455,6 @@
],
"pipeline_schedules":[
],
"services":[
],
"protected_branches":[

View File

@ -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": [

View File

@ -32,7 +32,6 @@
],
"labels": [],
"issues": [],
"services": [],
"snippets": [],
"hooks": []
}

View File

@ -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', () => {

View File

@ -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() {

View File

@ -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,
};

View File

@ -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']);
});
});
});
});

View File

@ -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&copy.yml',
});
expect(window.location.assign).toHaveBeenCalledWith(
'http://localhost/?dashboard=dashboard%2526copy.yml',
`${TEST_HOST}/?dashboard=dashboard%2526copy.yml`,
);
});
});

View File

@ -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: '',
});
},

View File

@ -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',
});
})

View File

@ -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 } });
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');
});
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);
});
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);
return wrapper.vm.$nextTick();
});
it(`has the ${icon} icon`, () => {
expect(finderFunction().props('icon')).toBe(icon);
it('has 3 details rows', () => {
expect(findDetailsRows().length).toBe(3);
});
it(`is ${clipboard} that clipboard button exist`, () => {
expect(
finderFunction()
.find(ClipboardButton)
.exists(),
).toBe(clipboard);
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 } });
await wrapper.vm.$nextTick();
expect(findDetailsRows().length).toBe(0);
});
});
});

View File

@ -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(/^\//, '')}`,
);
});
});

View File

@ -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.enableDropdown();
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.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');
});

View File

@ -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`);
});
});
});

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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