Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
ff1701e51d
commit
cdd71cf36a
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix comment loading error in issues and merge requests
|
||||
merge_request: 36043
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Exclude integrations (services) from Project Import/Export
|
||||
merge_request: 35249
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Replace fa-comment / fa-comments icons with GitLab SVG
|
||||
merge_request: 36206
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add broken tag state to tags list items
|
||||
merge_request: 36442
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Bring SAST to Core - eslint
|
||||
merge_request: 36392
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Restore the search autocomplete for groups/project/other
|
||||
merge_request: 35983
|
||||
author:
|
||||
type: other
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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/),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Binary file not shown.
|
@ -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
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -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": [
|
||||
{
|
||||
|
|
|
@ -455,9 +455,6 @@
|
|||
],
|
||||
"pipeline_schedules":[
|
||||
|
||||
],
|
||||
"services":[
|
||||
|
||||
],
|
||||
"protected_branches":[
|
||||
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -32,7 +32,6 @@
|
|||
],
|
||||
"labels": [],
|
||||
"issues": [],
|
||||
"services": [],
|
||||
"snippets": [],
|
||||
"hooks": []
|
||||
}
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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']);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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: '',
|
||||
});
|
||||
},
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
})
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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(/^\//, '')}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
|
@ -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`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue