Commit d80f3cd7 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent be81c157
<script>
import { GlPopover, GlSprintf, GlButton, GlIcon } from '@gitlab/ui';
import Cookies from 'js-cookie';
import { parseBoolean } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import { glEmojiTag } from '~/emoji';
export default {
components: {
GlPopover,
GlSprintf,
GlIcon,
GlButton,
},
props: {
target: {
type: String,
required: true,
},
cssClass: {
type: String,
required: true,
},
dismissKey: {
type: String,
required: true,
},
},
data() {
return {
popoverDismissed: parseBoolean(Cookies.get(this.dismissKey)),
};
},
computed: {
suggestTitle() {
return s__(`suggestPipeline|1/2: Choose a template`);
},
suggestContent() {
return s__(
`suggestPipeline|We recommend the %{boldStart}Code Quality%{boldEnd} template, which will add a report widget to your Merge Requests. This way you’ll learn about code quality degradations much sooner. %{footerStart} Goodbye technical debt! %{footerEnd}`,
);
},
emoji() {
return glEmojiTag('wave');
},
},
methods: {
onDismiss() {
this.popoverDismissed = true;
Cookies.set(this.dismissKey, this.popoverDismissed, { expires: 365 });
},
},
};
</script>
<template>
<gl-popover
v-if="!popoverDismissed"
show
:target="target"
placement="rightbottom"
trigger="manual"
container="viewport"
:css-classes="[cssClass]"
>
<template #title>
<gl-button :aria-label="__('Close')" class="btn-blank float-right" @click="onDismiss">
<gl-icon name="close" aria-hidden="true" />
</gl-button>
{{ suggestTitle }}
</template>
<gl-sprintf :message="suggestContent">
<template #bold="{content}">
<strong> {{ content }} </strong>
</template>
<template #footer="{content}">
<div class="mt-3">
{{ content }}
<span v-html="emoji"></span>
</div>
</template>
</gl-sprintf>
</gl-popover>
</template>
import Vue from 'vue';
import Popover from './components/popover.vue';
export default el =>
new Vue({
el,
render(createElement) {
return createElement(Popover, {
props: {
target: el.dataset.target,
cssClass: el.dataset.cssClass,
dismissKey: el.dataset.dismissKey,
},
});
},
});
// Browser polyfills // Browser polyfills
/**
* Polyfill: fetch
* @what https://fetch.spec.whatwg.org/
* @why Because Apollo GraphQL client relies on fetch
* @browsers Internet Explorer 11
* @see https://caniuse.com/#feat=fetch
*/
import 'unfetch/polyfill/index';
/**
* Polyfill: FormData APIs
* @what delete(), get(), getAll(), has(), set(), entries(), keys(), values(),
* and support for for...of
* @why Because Apollo GraphQL client relies on fetch
* @browsers Internet Explorer 11, Edge < 18
* @see https://caniuse.com/#feat=mdn-api_formdata and subfeatures
*/
import 'formdata-polyfill'; import 'formdata-polyfill';
import './polyfills/custom_event'; import './polyfills/custom_event';
import './polyfills/element'; import './polyfills/element';
import './polyfills/event'; import './polyfills/event';
......
/**
* Polyfill: CustomEvent constructor
* @what new CustomEvent()
* @why Certain features, e.g. notes utilize this
* @browsers Internet Explorer 11
* @see https://caniuse.com/#feat=customevent
*/
if (typeof window.CustomEvent !== 'function') { if (typeof window.CustomEvent !== 'function') {
window.CustomEvent = function CustomEvent(event, params) { window.CustomEvent = function CustomEvent(event, params) {
const evt = document.createEvent('CustomEvent'); const evt = document.createEvent('CustomEvent');
......
// polyfill Element.classList and DOMTokenList with classList.js /**
* Polyfill
* @what Element.classList
* @why In order to align browser features
* @browsers Internet Explorer 11
* @see https://caniuse.com/#feat=classlist
*/
import 'classlist-polyfill'; import 'classlist-polyfill';
/**
* Polyfill
* @what Element.closest
* @why In order to align browser features
* @browsers Internet Explorer 11
* @see https://caniuse.com/#feat=element-closest
*/
Element.prototype.closest = Element.prototype.closest =
Element.prototype.closest || Element.prototype.closest ||
function closest(selector, selectedElement = this) { function closest(selector, selectedElement = this) {
...@@ -10,6 +23,13 @@ Element.prototype.closest = ...@@ -10,6 +23,13 @@ Element.prototype.closest =
: Element.prototype.closest(selector, selectedElement.parentElement); : Element.prototype.closest(selector, selectedElement.parentElement);
}; };
/**
* Polyfill
* @what Element.matches
* @why In order to align browser features
* @browsers Internet Explorer 11
* @see https://caniuse.com/#feat=mdn-api_element_matches
*/
Element.prototype.matches = Element.prototype.matches =
Element.prototype.matches || Element.prototype.matches ||
Element.prototype.matchesSelector || Element.prototype.matchesSelector ||
...@@ -26,7 +46,15 @@ Element.prototype.matches = ...@@ -26,7 +46,15 @@ Element.prototype.matches =
return i > -1; return i > -1;
}; };
// From the polyfill on MDN, https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/remove#Polyfill /**
* Polyfill
* @what ChildNode.remove, Element.remove, CharacterData.remove, DocumentType.remove
* @why In order to align browser features
* @browsers Internet Explorer 11
* @see https://caniuse.com/#feat=childnode-remove
*
* From the polyfill on MDN, https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/remove#Polyfill
*/
(arr => { (arr => {
arr.forEach(item => { arr.forEach(item => {
if (Object.prototype.hasOwnProperty.call(item, 'remove')) { if (Object.prototype.hasOwnProperty.call(item, 'remove')) {
......
/** /**
* Polyfill for IE11 support. * Polyfill: Event constructor
* new Event() is not supported by IE11. * @what new Event()
* @why To align browser support
* @browsers Internet Explorer 11
* @see https://caniuse.com/#feat=mdn-api_event_event
*
* Although `initEvent` is deprecated for modern browsers it is the one supported by IE * Although `initEvent` is deprecated for modern browsers it is the one supported by IE
*/ */
if (typeof window.Event !== 'function') { if (typeof window.Event !== 'function') {
......
/**
* Polyfill
* @what NodeList.forEach
* @why To align browser support
* @browsers Internet Explorer 11
* @see https://caniuse.com/#feat=mdn-api_nodelist_foreach
*/
if (window.NodeList && !NodeList.prototype.forEach) { if (window.NodeList && !NodeList.prototype.forEach) {
NodeList.prototype.forEach = function forEach(callback, thisArg = window) { NodeList.prototype.forEach = function forEach(callback, thisArg = window) {
for (let i = 0; i < this.length; i += 1) { for (let i = 0; i < this.length; i += 1) {
......
/**
* Polyfill
* @what requestIdleCallback
* @why To align browser features
* @browsers Safari (all versions), Internet Explorer 11
* @see https://caniuse.com/#feat=requestidlecallback
*/
window.requestIdleCallback = window.requestIdleCallback =
window.requestIdleCallback || window.requestIdleCallback ||
function requestShim(cb) { function requestShim(cb) {
......
/**
* polyfill support for external SVG file references via <use xlink:href>
* @what polyfill support for external SVG file references via <use xlink:href>
* @why This is used in our GitLab SVG icon library
* @browsers Internet Explorer 11
* @see https://caniuse.com/#feat=mdn-svg_elements_use_external_uri
* @see https//css-tricks.com/svg-use-external-source/
*/
import svg4everybody from 'svg4everybody'; import svg4everybody from 'svg4everybody';
// polyfill support for external SVG file references via <use xlink:href>
// @see https://css-tricks.com/svg-use-external-source/
svg4everybody(); svg4everybody();
import _ from 'underscore'; import { uniq } from 'lodash';
import emojiMap from 'emojis/digests.json'; import emojiMap from 'emojis/digests.json';
import emojiAliases from 'emojis/aliases.json'; import emojiAliases from 'emojis/aliases.json';
...@@ -18,7 +18,7 @@ export function filterEmojiNames(filter) { ...@@ -18,7 +18,7 @@ export function filterEmojiNames(filter) {
} }
export function filterEmojiNamesByAlias(filter) { export function filterEmojiNamesByAlias(filter) {
return _.uniq(filterEmojiNames(filter).map(name => normalizeEmojiName(name))); return uniq(filterEmojiNames(filter).map(name => normalizeEmojiName(name)));
} }
let emojiCategoryMap; let emojiCategoryMap;
......
...@@ -472,33 +472,6 @@ export default class FilteredSearchManager { ...@@ -472,33 +472,6 @@ export default class FilteredSearchManager {
}); });
input.value = input.value.replace(`${tokenKey}:`, ''); input.value = input.value.replace(`${tokenKey}:`, '');
} }
const splitSearchToken = searchToken && searchToken.split(' ');
let lastSearchToken = _.last(splitSearchToken);
lastSearchToken = lastSearchToken?.toLowerCase();
/**
* If user writes "milestone", a known token, in the input, we should not
* wait for leading colon to flush it as a filter token.
*/
if (this.filteredSearchTokenKeys.getKeys().includes(lastSearchToken)) {
if (splitSearchToken.length > 1) {
splitSearchToken.pop();
const searchVisualTokens = splitSearchToken.join(' ');
input.value = input.value.replace(searchVisualTokens, '');
FilteredSearchVisualTokens.addSearchVisualToken(searchVisualTokens);
}
FilteredSearchVisualTokens.addFilterVisualToken(lastSearchToken, null, null, {
uppercaseTokenName: this.filteredSearchTokenKeys.shouldUppercaseTokenName(
lastSearchToken,
),
capitalizeTokenValue: this.filteredSearchTokenKeys.shouldCapitalizeTokenValue(
lastSearchToken,
),
});
input.value = input.value.replace(lastSearchToken, '');
}
} else if (!isLastVisualTokenValid && !FilteredSearchVisualTokens.getLastTokenOperator()) { } else if (!isLastVisualTokenValid && !FilteredSearchVisualTokens.getLastTokenOperator()) {
const tokenKey = FilteredSearchVisualTokens.getLastTokenPartial(); const tokenKey = FilteredSearchVisualTokens.getLastTokenPartial();
const tokenOperator = searchToken && searchToken.trim(); const tokenOperator = searchToken && searchToken.trim();
......
...@@ -5,6 +5,7 @@ import { getSvgIconPathContent } from '~/lib/utils/icon_utils'; ...@@ -5,6 +5,7 @@ import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
import { chartHeight } from '../../constants'; import { chartHeight } from '../../constants';
import { makeDataSeries } from '~/helpers/monitor_helper'; import { makeDataSeries } from '~/helpers/monitor_helper';
import { graphDataValidatorForValues } from '../../utils'; import { graphDataValidatorForValues } from '../../utils';
import { getYAxisOptions, getChartGrid } from './options';
export default { export default {
components: { components: {
...@@ -41,15 +42,25 @@ export default { ...@@ -41,15 +42,25 @@ export default {
values: queryData[0].data, values: queryData[0].data,
}; };
}, },
chartOptions() {
const yAxis = {
...getYAxisOptions(this.graphData.yAxis),
scale: false,
};
return {
grid: getChartGrid(),
yAxis,
dataZoom: this.dataZoomConfig,
};
},
xAxisTitle() { xAxisTitle() {
return this.graphData.metrics[0].result[0].x_label !== undefined return this.graphData.metrics[0].result[0].x_label !== undefined
? this.graphData.metrics[0].result[0].x_label ? this.graphData.metrics[0].result[0].x_label
: ''; : '';
}, },
yAxisTitle() { yAxisTitle() {
return this.graphData.metrics[0].result[0].y_label !== undefined return this.chartOptions.yAxis.name;
? this.graphData.metrics[0].result[0].y_label
: '';
}, },
xAxisType() { xAxisType() {
return this.graphData.x_type !== undefined ? this.graphData.x_type : 'category'; return this.graphData.x_type !== undefined ? this.graphData.x_type : 'category';
...@@ -59,11 +70,6 @@ export default { ...@@ -59,11 +70,6 @@ export default {
return handleIcon ? { handleIcon } : {}; return handleIcon ? { handleIcon } : {};
}, },
chartOptions() {
return {
dataZoom: this.dataZoomConfig,
};
},
}, },
created() { created() {
this.setSvg('scroll-handle'); this.setSvg('scroll-handle');
......
...@@ -79,7 +79,7 @@ export const fetchData = ({ dispatch }) => { ...@@ -79,7 +79,7 @@ export const fetchData = ({ dispatch }) => {
dispatch('fetchEnvironmentsData'); dispatch('fetchEnvironmentsData');
}; };
export const fetchDashboard = ({ state, dispatch }) => { export const fetchDashboard = ({ state, commit, dispatch }) => {
dispatch('requestMetricsDashboard'); dispatch('requestMetricsDashboard');
const params = {}; const params = {};
...@@ -100,6 +100,7 @@ export const fetchDashboard = ({ state, dispatch }) => { ...@@ -100,6 +100,7 @@ export const fetchDashboard = ({ state, dispatch }) => {
.catch(error => { .catch(error => {
Sentry.captureException(error); Sentry.captureException(error);
commit(types.SET_ALL_DASHBOARDS, error.response?.data?.all_dashboards ?? []);
dispatch('receiveMetricsDashboardFailure', error); dispatch('receiveMetricsDashboardFailure', error);
if (state.showErrorBanner) { if (state.showErrorBanner) {
......
import initBlobBundle from '~/blob_edit/blob_bundle'; import initBlobBundle from '~/blob_edit/blob_bundle';
import initPopover from '~/blob/suggest_gitlab_ci_yml';
document.addEventListener('DOMContentLoaded', initBlobBundle); document.addEventListener('DOMContentLoaded', () => {
initBlobBundle();
const suggestEl = document.querySelector('.js-suggest-gitlab-ci-yml');
if (suggestEl) {
initPopover(suggestEl);
}
});
import initShowRelease from '~/releases/mount_show';
document.addEventListener('DOMContentLoaded', initShowRelease);
<script> <script>
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import { GlButton, GlFormInput, GlFormGroup } from '@gitlab/ui'; import { GlButton, GlLink, GlFormInput, GlFormGroup } from '@gitlab/ui';
import { escape as esc } from 'lodash'; import { escape as esc } from 'lodash';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import MarkdownField from '~/vue_shared/components/markdown/field.vue'; import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow'; import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import { BACK_URL_PARAM } from '~/releases/constants';
import { getParameterByName } from '~/lib/utils/common_utils';
export default { export default {
name: 'ReleaseEditApp', name: 'ReleaseEditApp',
...@@ -12,6 +14,7 @@ export default { ...@@ -12,6 +14,7 @@ export default {
GlFormInput, GlFormInput,
GlFormGroup, GlFormGroup,
GlButton, GlButton,
GlLink,
MarkdownField, MarkdownField,
}, },
directives: { directives: {
...@@ -74,6 +77,9 @@ export default { ...@@ -74,6 +77,9 @@ export default {
this.updateReleaseNotes(notes); this.updateReleaseNotes(notes);
}, },
}, },
cancelPath() {
return getParameterByName(BACK_URL_PARAM) || this.releasesPagePath;
},
}, },
created() { created() {
this.fetchRelease(); this.fetchRelease();
...@@ -84,7 +90,6 @@ export default { ...@@ -84,7 +90,6 @@ export default {
'updateRelease', 'updateRelease',
'updateReleaseTitle', 'updateReleaseTitle',
'updateReleaseNotes', 'updateReleaseNotes',
'navigateToReleasesPage',
]), ]),
}, },
}; };
...@@ -157,15 +162,9 @@ export default { ...@@ -157,15 +162,9 @@ export default {
> >
{{ __('Save changes') }} {{ __('Save changes') }}
</gl-button> </gl-button>
<gl-button <gl-link :href="cancelPath" class="js-cancel-button btn btn-default">
class="js-cancel-button"
variant="default"
type="button"
:aria-label="__('Cancel')"
@click="navigateToReleasesPage()"
>
{{ __('Cancel') }} {{ __('Cancel') }}
</gl-button> </gl-link>
</div> </div>
</form> </form>
</div> </div>
......
<script>
import { mapState, mapActions } from 'vuex';
import { GlSkeletonLoading } from '@gitlab/ui';
import ReleaseBlock from './release_block.vue';
export default {
name: 'ReleaseShowApp',
components: {
GlSkeletonLoading,
ReleaseBlock,
},
computed: {
...mapState('detail', ['isFetchingRelease', 'fetchError', 'release']),
},
created() {
this.fetchRelease();
},
methods: {
...mapActions('detail', ['fetchRelease']),
},
};
</script>
<template>
<div class="prepend-top-default">
<gl-skeleton-loading v-if="isFetchingRelease" />
<release-block v-else-if="!fetchError" :release="release" />
</div>
</template>
<script> <script>
import { GlTooltipDirective, GlLink, GlBadge } from '@gitlab/ui'; import { GlTooltipDirective, GlLink, GlBadge } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { BACK_URL_PARAM } from '~/releases/constants';
import { setUrlParams } from '~/lib/utils/url_utility';
export default { export default {
name: 'ReleaseBlockHeader', name: 'ReleaseBlockHeader',
...@@ -20,7 +22,15 @@ export default { ...@@ -20,7 +22,15 @@ export default {
}, },
computed: { computed: {
editLink() { editLink() {
return this.release._links?.editUrl; if (this.release._links?.editUrl) {
const queryParams = {
[BACK_URL_PARAM]: window.location.href,
};
return setUrlParams(queryParams, this.release._links.editUrl);
}
return undefined;
}, },
selfLink() { selfLink() {
return this.release._links?.self; return this.release._links?.self;
......
/* eslint-disable import/prefer-default-export */
// This eslint-disable ^^^ can be removed when at least
// one more constant is added to this file. Currently
// constants.js files with only a single constant
// are flagged by this rule.
export const MAX_MILESTONES_TO_DISPLAY = 5; export const MAX_MILESTONES_TO_DISPLAY = 5;
export const BACK_URL_PARAM = 'back_url';
...@@ -6,7 +6,15 @@ import detailModule from './stores/modules/detail'; ...@@ -6,7 +6,15 @@ import detailModule from './stores/modules/detail';
export default () => { export default () => {
const el = document.getElementById('js-edit-release-page'); const el = document.getElementById('js-edit-release-page');
const store = createStore({ detail: detailModule }); const store = createStore({
modules: {
detail: detailModule,
},
featureFlags: {
releaseShowPage: Boolean(gon.features?.releaseShowPage),
},
});
store.dispatch('detail/setInitialState', el.dataset); store.dispatch('detail/setInitialState', el.dataset);
return new Vue({ return new Vue({
......
...@@ -8,7 +8,11 @@ export default () => { ...@@ -8,7 +8,11 @@ export default () => {
return new Vue({ return new Vue({
el, el,
store: createStore({ list: listModule }), store: createStore({
modules: {
list: listModule,
},
}),
render: h => render: h =>
h(ReleaseListApp, { h(ReleaseListApp, {
props: { props: {
......
import Vue from 'vue';
import ReleaseShowApp from './components/app_show.vue';
import createStore from './stores';
import detailModule from './stores/modules/detail';
export default () => {
const el = document.getElementById('js-show-release-page');
const store = createStore({
modules: {
detail: detailModule,
},
});
store.dispatch('detail/setInitialState', el.dataset);
return new Vue({
el,
store,
render: h => h(ReleaseShowApp),
});
};
...@@ -3,4 +3,8 @@ import Vuex from 'vuex'; ...@@ -3,4 +3,8 @@ import Vuex from 'vuex';
Vue.use(Vuex); Vue.use(Vuex);
export default modules => new Vuex.Store({ modules }); export default ({ modules, featureFlags }) =>
new Vuex.Store({
modules,
state: { featureFlags },
});
...@@ -33,9 +33,11 @@ export const updateReleaseTitle = ({ commit }, title) => commit(types.UPDATE_REL ...@@ -33,9 +33,11 @@ export const updateReleaseTitle = ({ commit }, title) => commit(types.UPDATE_REL
export const updateReleaseNotes = ({ commit }, notes) => commit(types.UPDATE_RELEASE_NOTES, notes); export const updateReleaseNotes = ({ commit }, notes) => commit(types.UPDATE_RELEASE_NOTES, notes);
export const requestUpdateRelease = ({ commit }) => commit(types.REQUEST_UPDATE_RELEASE); export const requestUpdateRelease = ({ commit }) => commit(types.REQUEST_UPDATE_RELEASE);
export const receiveUpdateReleaseSuccess = ({ commit, dispatch }) => { export const receiveUpdateReleaseSuccess = ({ commit, state, rootState }) => {
commit(types.RECEIVE_UPDATE_RELEASE_SUCCESS); commit(types.RECEIVE_UPDATE_RELEASE_SUCCESS);
dispatch('navigateToReleasesPage'); redirectTo(
rootState.featureFlags.releaseShowPage ? state.release._links.self : state.releasesPagePath,
);
}; };
export const receiveUpdateReleaseError = ({ commit }, error) => { export const receiveUpdateReleaseError = ({ commit }, error) => {
commit(types.RECEIVE_UPDATE_RELEASE_ERROR, error); commit(types.RECEIVE_UPDATE_RELEASE_ERROR, error);
......
...@@ -138,3 +138,15 @@ ...@@ -138,3 +138,15 @@
max-width: 40%; max-width: 40%;
} }
} }
.suggest-gitlab-ci-yml {
margin-top: -1em;
.popover-header {
padding: $gl-padding;
.ic-close {
margin-top: -1em;
}
}
}
...@@ -81,6 +81,7 @@ $gl-gray-400: #999; ...@@ -81,6 +81,7 @@ $gl-gray-400: #999;
$gl-gray-500: #777; $gl-gray-500: #777;
$gl-gray-600: #666; $gl-gray-600: #666;
$gl-gray-700: #555; $gl-gray-700: #555;
$gl-gray-800: #333;
$green-50: #f1fdf6; $green-50: #f1fdf6;
$green-100: #dcf5e7; $green-100: #dcf5e7;
......
...@@ -18,3 +18,19 @@ p.details { ...@@ -18,3 +18,19 @@ p.details {
pre.commit-message { pre.commit-message {
white-space: pre-wrap; white-space: pre-wrap;
} }
.gl-label-scoped {
box-shadow: 0 0 0 2px currentColor inset;
}
.gl-label-text {
padding: 0 5px;
}
.gl-label-text-light {
color: $white-light;
}
.gl-label-text-dark {
color: $gl-gray-800;
}
...@@ -1006,6 +1006,14 @@ pre.light-well { ...@@ -1006,6 +1006,14 @@ pre.light-well {
} }
} }
&:not(.with-pipeline-status) {
.icon-wrapper:first-of-type {
@include media-breakpoint-up(lg) {
margin-left: $gl-padding-32;
}
}
}
.ci-status-link { .ci-status-link {
display: inline-flex; display: inline-flex;
} }
......
...@@ -79,7 +79,7 @@ module MarkupHelper ...@@ -79,7 +79,7 @@ module MarkupHelper
md = markdown_field(object, attribute, options.merge(post_process: false)) md = markdown_field(object, attribute, options.merge(post_process: false))
return unless md.present? return unless md.present?
tags = %w(a gl-emoji b pre code p span) tags = %w(a gl-emoji b strong i em pre code p span)
tags << 'img' if options[:allow_images] tags << 'img' if options[:allow_images]
text = truncate_visible(md, max_chars || md.length) text = truncate_visible(md, max_chars || md.length)
...@@ -88,7 +88,7 @@ module MarkupHelper ...@@ -88,7 +88,7 @@ module MarkupHelper
text, text,
tags: tags, tags: tags,
attributes: Rails::Html::WhiteListSanitizer.allowed_attributes + attributes: Rails::Html::WhiteListSanitizer.allowed_attributes +
%w(style data-src data-name data-unicode-version data-iid data-project-path data-mr-title) %w(style data-src data-name data-unicode-version data-iid data-project-path data-mr-title data-html)
) )
# since <img> tags are stripped, this can leave empty <a> tags hanging around # since <img> tags are stripped, this can leave empty <a> tags hanging around
...@@ -233,7 +233,7 @@ module MarkupHelper ...@@ -233,7 +233,7 @@ module MarkupHelper
def strip_empty_link_tags(text) def strip_empty_link_tags(text)
scrubber = Loofah::Scrubber.new do |node| scrubber = Loofah::Scrubber.new do |node|
node.remove if node.name == 'a' && node.content.blank? node.remove if node.name == 'a' && node.children.empty?
end end
sanitize text, scrubber: scrubber sanitize text, scrubber: scrubber
......
# frozen_string_literal: true
module SuggestPipelineHelper
def should_suggest_gitlab_ci_yml?
Feature.enabled?(:suggest_pipeline) &&
current_user &&
params[:suggest_gitlab_ci_yml] == 'true'
end
end
...@@ -305,6 +305,10 @@ class Issue < ApplicationRecord ...@@ -305,6 +305,10 @@ class Issue < ApplicationRecord
labels.map(&:hook_attrs) labels.map(&:hook_attrs)
end end
def previous_updated_at
previous_changes['updated_at']&.first || updated_at
end
private private
def ensure_metrics def ensure_metrics
......
...@@ -12,6 +12,7 @@ class MilestoneNote < ::Note ...@@ -12,6 +12,7 @@ class MilestoneNote < ::Note
created_at: event.created_at, created_at: event.created_at,
noteable: resource, noteable: resource,
milestone: event.milestone, milestone: event.milestone,
discussion_id: event.discussion_id,
event: event, event: event,
system_note_metadata: ::SystemNoteMetadata.new(action: 'milestone'), system_note_metadata: ::SystemNoteMetadata.new(action: 'milestone'),
resource_parent: resource_parent resource_parent: resource_parent
......
...@@ -21,7 +21,7 @@ class ResourceEvent < ApplicationRecord ...@@ -21,7 +21,7 @@ class ResourceEvent < ApplicationRecord
private private
def discussion_id_key def discussion_id_key
[self.class.name, created_at, user_id] [self.class.name, id, user_id]
end end
def exactly_one_issuable def exactly_one_issuable
......
...@@ -103,6 +103,10 @@ class ResourceLabelEvent < ResourceEvent ...@@ -103,6 +103,10 @@ class ResourceLabelEvent < ResourceEvent
def resource_parent def resource_parent
issuable.project || issuable.group issuable.project || issuable.group
end end
def discussion_id_key
[self.class.name, created_at, user_id]
end
end end
ResourceLabelEvent.prepend_if_ee('EE::ResourceLabelEvent') ResourceLabelEvent.prepend_if_ee('EE::ResourceLabelEvent')
= render 'shared/projects/list', projects: @projects, user: current_user = render 'shared/projects/list', projects: @projects, pipeline_status: Feature.enabled?(:dashboard_pipeline_status, default_enabled: true), user: current_user
- is_explore_page = defined?(explore_page) && explore_page - is_explore_page = defined?(explore_page) && explore_page
= render 'shared/projects/list', projects: projects, user: current_user, explore_page: is_explore_page = render 'shared/projects/list', projects: projects, user: current_user, explore_page: is_explore_page, pipeline_status: Feature.enabled?(:dashboard_pipeline_status, default_enabled: true)
...@@ -17,8 +17,10 @@ ...@@ -17,8 +17,10 @@
%span.pull-left.append-right-10 %span.pull-left.append-right-10
\/ \/
= text_field_tag 'file_name', params[:file_name], placeholder: "File name", = text_field_tag 'file_name', params[:file_name], placeholder: "File name",
required: true, class: 'form-control new-file-name js-file-path-name-input' required: true, class: 'form-control new-file-name js-file-path-name-input', value: params[:file_name] || (should_suggest_gitlab_ci_yml? ? '.gitlab-ci.yml' : '')
= render 'template_selectors' = render 'template_selectors'
- if should_suggest_gitlab_ci_yml?
= render partial: 'suggest_gitlab_ci_yml', locals: { target: '#gitlab-ci-yml-selector', dismiss_key: "suggest_gitlab_ci_yml_#{@project.id}" }
.file-buttons .file-buttons
- if is_markdown - if is_markdown
......
.js-suggest-gitlab-ci-yml{ data: { toggle: 'popover',
target: target,
css_class: 'suggest-gitlab-ci-yml ml-4',
dismiss_key: dismiss_key } }
.template-selectors-menu.gl-pl-2 .template-selectors-menu.gl-pl-2
.template-selector-dropdowns-wrap .template-selector-dropdowns-wrap
.template-type-selector.js-template-type-selector-wrap.hidden .template-type-selector.js-template-type-selector-wrap.hidden
= dropdown_tag(_("Select a template type"), options: { toggle_class: 'js-template-type-selector qa-template-type-dropdown', dropdown_class: 'dropdown-menu-selectable'} ) - toggle_text = should_suggest_gitlab_ci_yml? ? '.gitlab-ci.yml' : 'Select a template type'
= dropdown_tag(_(toggle_text), options: { toggle_class: 'js-template-type-selector qa-template-type-dropdown', dropdown_class: 'dropdown-menu-selectable' })
.license-selector.js-license-selector-wrap.js-template-selector-wrap.hidden .license-selector.js-license-selector-wrap.js-template-selector-wrap.hidden
= dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-license-selector qa-license-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: licenses_for_select(@project), project: @project.name, fullname: @project.namespace.human_name } } ) = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-license-selector qa-license-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: licenses_for_select(@project), project: @project.name, fullname: @project.namespace.human_name } } )
.gitignore-selector.js-gitignore-selector-wrap.js-template-selector-wrap.hidden .gitignore-selector.js-gitignore-selector-wrap.js-template-selector-wrap.hidden
= dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-gitignore-selector qa-gitignore-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitignore_names(@project) } } ) = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-gitignore-selector qa-gitignore-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitignore_names(@project) } } )
.gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.js-template-selector-wrap.hidden #gitlab-ci-yml-selector.gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.js-template-selector-wrap.hidden
= dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-gitlab-ci-yml-selector qa-gitlab-ci-yml-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls(@project) } } ) = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-gitlab-ci-yml-selector qa-gitlab-ci-yml-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls(@project) } } )
.dockerfile-selector.js-dockerfile-selector-wrap.js-template-selector-wrap.hidden .dockerfile-selector.js-dockerfile-selector-wrap.js-template-selector-wrap.hidden
= dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-dockerfile-selector qa-dockerfile-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: dockerfile_names(@project) } } ) = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-dockerfile-selector qa-dockerfile-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: dockerfile_names(@project) } } )
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
= runner_status_icon(runner) = runner_status_icon(runner)
- if @project_runners.include?(runner) - if @project_runners.include?(runner)
= link_to runner.short_sha.concat("..."), project_runner_path(@project, runner), class: 'commit-sha has-tooltip', title: _("Partial token for reference only") = link_to _("%{token}...") % { token: runner.short_sha }, project_runner_path(@project, runner), class: 'commit-sha has-tooltip', title: _("Partial token for reference only")
- if runner.locked? - if runner.locked?
= icon('lock', class: 'has-tooltip', title: _('Locked to current projects')) = icon('lock', class: 'has-tooltip', title: _('Locked to current projects'))
......
...@@ -12,7 +12,9 @@ ...@@ -12,7 +12,9 @@
- css_class += " no-description" if project.description.blank? && !show_last_commit_as_description - css_class += " no-description" if project.description.blank? && !show_last_commit_as_description
- cache_key = project_list_cache_key(project, pipeline_status: pipeline_status) - cache_key = project_list_cache_key(project, pipeline_status: pipeline_status)
- updated_tooltip = time_ago_with_tooltip(project.last_activity_date) - updated_tooltip = time_ago_with_tooltip(project.last_activity_date)
- show_pipeline_status_icon = pipeline_status && can?(current_user, :read_cross_project) && project.pipeline_status.has_status? && can?(current_user, :read_build, project)
- css_controls_class = compact_mode ? [] : ["flex-lg-row", "justify-content-lg-between"] - css_controls_class = compact_mode ? [] : ["flex-lg-row", "justify-content-lg-between"]
- css_controls_class << "with-pipeline-status" if show_pipeline_status_icon
- avatar_container_class = project.creator && use_creator_avatar ? '' : 'rect-avatar' - avatar_container_class = project.creator && use_creator_avatar ? '' : 'rect-avatar'
- license_name = project_license_name(project) - license_name = project_license_name(project)
...@@ -61,6 +63,11 @@ ...@@ -61,6 +63,11 @@
.controls.d-flex.flex-sm-column.align-items-center.align-items-sm-end.flex-wrap.flex-shrink-0.text-secondary{ class: css_controls_class.join(" ") } .controls.d-flex.flex-sm-column.align-items-center.align-items-sm-end.flex-wrap.flex-shrink-0.text-secondary{ class: css_controls_class.join(" ") }
.icon-container.d-flex.align-items-center .icon-container.d-flex.align-items-center
- if show_pipeline_status_icon
- pipeline_path = pipelines_project_commit_path(project.pipeline_status.project, project.pipeline_status.sha, ref: project.pipeline_status.ref)
%span.icon-wrapper.pipeline-status
= render 'ci/status/icon', status: project.last_pipeline.detailed_status(current_user), tooltip_placement: 'top', path: pipeline_path
= render_if_exists 'shared/projects/archived', project: project = render_if_exists 'shared/projects/archived', project: project
- if stars - if stars
= link_to project_starrers_path(project), = link_to project_starrers_path(project),
......
---
title: Ensure weight changes no longer render duplicate system notes
merge_request: 26014
author:
type: fixed
---
title: Use y-axis format configuration in column charts
merge_request: 26356
author:
type: changed
---
title: Fix scoped labels rendering in To-Do List
merge_request: 26146
author:
type: fixed
---
title: Fix scoped labels rendering in emails
merge_request: 26347
author:
type: fixed
---
title: Fix an error with concat method
merge_request:
author:
type: fixed
---
title: Fix dashboards dropdown if custom dashboard is broken
merge_request: 26228
author:
type: fixed
---
title: Nudge users to select a gitlab-ci.yml template
merge_request: 24622
author:
type: added
---
title: Update renewal banner link for clearer instructions
merge_request: 26240
author:
type: changed
---
title: Polyfill fetch for Internet Explorer 11
merge_request: 26366
author:
type: fixed
---
title: Show CI status in project dashboards
merge_request: 26403
author:
type: fixed
---
title: Use colon to tokenize input in filtered search
merge_request: 26072
author:
type: changed
...@@ -484,6 +484,9 @@ msgstr "" ...@@ -484,6 +484,9 @@ msgstr ""
msgid "%{title} changes" msgid "%{title} changes"
msgstr "" msgstr ""
msgid "%{token}..."
msgstr ""
msgid "%{totalWeight} total weight" msgid "%{totalWeight} total weight"
msgstr "" msgstr ""
...@@ -1281,6 +1284,9 @@ msgstr "" ...@@ -1281,6 +1284,9 @@ msgstr ""
msgid "Admin notes" msgid "Admin notes"
msgstr "" msgstr ""
msgid "AdminArea|Bots"
msgstr ""
msgid "AdminArea|Included Free in license" msgid "AdminArea|Included Free in license"
msgstr "" msgstr ""
...@@ -24074,6 +24080,12 @@ msgstr "" ...@@ -24074,6 +24080,12 @@ msgstr ""
msgid "success" msgid "success"
msgstr "" msgstr ""
msgid "suggestPipeline|1/2: Choose a template"
msgstr ""
msgid "suggestPipeline|We recommend the %{boldStart}Code Quality%{boldEnd} template, which will add a report widget to your Merge Requests. This way you’ll learn about code quality degradations much sooner. %{footerStart} Goodbye technical debt! %{footerEnd}"
msgstr ""
msgid "syntax is correct" msgid "syntax is correct"
msgstr "" msgstr ""
......
...@@ -57,7 +57,7 @@ describe "Admin Runners" do ...@@ -57,7 +57,7 @@ describe "Admin Runners" do
expect(page).to have_content 'runner-active' expect(page).to have_content 'runner-active'
expect(page).to have_content 'runner-paused' expect(page).to have_content 'runner-paused'
input_filtered_search_keys('status=active') input_filtered_search_keys('status:=active')
expect(page).to have_content 'runner-active' expect(page).to have_content 'runner-active'
expect(page).not_to have_content 'runner-paused' expect(page).not_to have_content 'runner-paused'
end end
...@@ -68,7 +68,7 @@ describe "Admin Runners" do ...@@ -68,7 +68,7 @@ describe "Admin Runners" do
visit admin_runners_path visit admin_runners_path
input_filtered_search_keys('status=offline') input_filtered_search_keys('status:=offline')
expect(page).not_to have_content 'runner-active' expect(page).not_to have_content 'runner-active'
expect(page).not_to have_content 'runner-paused' expect(page).not_to have_content 'runner-paused'
...@@ -83,12 +83,12 @@ describe "Admin Runners" do ...@@ -83,12 +83,12 @@ describe "Admin Runners" do
visit admin_runners_path visit admin_runners_path
input_filtered_search_keys('status=active') input_filtered_search_keys('status:=active')
expect(page).to have_content 'runner-a-1' expect(page).to have_content 'runner-a-1'
expect(page).to have_content 'runner-b-1' expect(page).to have_content 'runner-b-1'
expect(page).not_to have_content 'runner-a-2' expect(page).not_to have_content 'runner-a-2'
input_filtered_search_keys('status=active runner-a') input_filtered_search_keys('status:=active runner-a')
expect(page).to have_content 'runner-a-1' expect(page).to have_content 'runner-a-1'
expect(page).not_to have_content 'runner-b-1' expect(page).not_to have_content 'runner-b-1'
expect(page).not_to have_content 'runner-a-2' expect(page).not_to have_content 'runner-a-2'
...@@ -105,7 +105,7 @@ describe "Admin Runners" do ...@@ -105,7 +105,7 @@ describe "Admin Runners" do
expect(page).to have_content 'runner-project' expect(page).to have_content 'runner-project'
expect(page).to have_content 'runner-group' expect(page).to have_content 'runner-group'
input_filtered_search_keys('type=project_type') input_filtered_search_keys('type:=project_type')
expect(page).to have_content 'runner-project' expect(page).to have_content 'runner-project'
expect(page).not_to have_content 'runner-group' expect(page).not_to have_content 'runner-group'
end end
...@@ -116,7 +116,7 @@ describe "Admin Runners" do ...@@ -116,7 +116,7 @@ describe "Admin Runners" do
visit admin_runners_path visit admin_runners_path
input_filtered_search_keys('type=instance_type') input_filtered_search_keys('type:=instance_type')
expect(page).not_to have_content 'runner-project' expect(page).not_to have_content 'runner-project'
expect(page).not_to have_content 'runner-group' expect(page).not_to have_content 'runner-group'
...@@ -131,12 +131,12 @@ describe "Admin Runners" do ...@@ -131,12 +131,12 @@ describe "Admin Runners" do
visit admin_runners_path visit admin_runners_path
input_filtered_search_keys('type=project_type') input_filtered_search_keys('type:=project_type')
expect(page).to have_content 'runner-a-1' expect(page).to have_content 'runner-a-1'
expect(page).to have_content 'runner-b-1' expect(page).to have_content 'runner-b-1'
expect(page).not_to have_content 'runner-a-2' expect(page).not_to have_content 'runner-a-2'
input_filtered_search_keys('type=project_type runner-a') input_filtered_search_keys('type:=project_type runner-a')
expect(page).to have_content 'runner-a-1' expect(page).to have_content 'runner-a-1'
expect(page).not_to have_content 'runner-b-1' expect(page).not_to have_content 'runner-b-1'
expect(page).not_to have_content 'runner-a-2' expect(page).not_to have_content 'runner-a-2'
...@@ -153,7 +153,7 @@ describe "Admin Runners" do ...@@ -153,7 +153,7 @@ describe "Admin Runners" do
expect(page).to have_content 'runner-blue' expect(page).to have_content 'runner-blue'
expect(page).to have_content 'runner-red' expect(page).to have_content 'runner-red'
input_filtered_search_keys('tag=blue') input_filtered_search_keys('tag:=blue')
expect(page).to have_content 'runner-blue' expect(page).to have_content 'runner-blue'
expect(page).not_to have_content 'runner-red' expect(page).not_to have_content 'runner-red'
...@@ -165,7 +165,7 @@ describe "Admin Runners" do ...@@ -165,7 +165,7 @@ describe "Admin Runners" do
visit admin_runners_path visit admin_runners_path
input_filtered_search_keys('tag=red') input_filtered_search_keys('tag:=red')
expect(page).not_to have_content 'runner-blue' expect(page).not_to have_content 'runner-blue'
expect(page).not_to have_content 'runner-blue' expect(page).not_to have_content 'runner-blue'
...@@ -179,13 +179,13 @@ describe "Admin Runners" do ...@@ -179,13 +179,13 @@ describe "Admin Runners" do
visit admin_runners_path visit admin_runners_path
input_filtered_search_keys('tag=blue') input_filtered_search_keys('tag:=blue')
expect(page).to have_content 'runner-a-1' expect(page).to have_content 'runner-a-1'
expect(page).to have_content 'runner-b-1' expect(page).to have_content 'runner-b-1'
expect(page).not_to have_content 'runner-a-2' expect(page).not_to have_content 'runner-a-2'
input_filtered_search_keys('tag=blue runner-a') input_filtered_search_keys('tag:=blue runner-a')
expect(page).to have_content 'runner-a-1' expect(page).to have_content 'runner-a-1'
expect(page).not_to have_content 'runner-b-1' expect(page).not_to have_content 'runner-b-1'
......
...@@ -624,7 +624,7 @@ describe 'Issue Boards', :js do ...@@ -624,7 +624,7 @@ describe 'Issue Boards', :js do
end end
def set_filter(type, text) def set_filter(type, text)
find('.filtered-search').native.send_keys("#{type}=#{text}") find('.filtered-search').native.send_keys("#{type}:=#{text}")
end end
def submit_filter def submit_filter
......
...@@ -211,7 +211,7 @@ describe 'Issue Boards add issue modal filtering', :js do ...@@ -211,7 +211,7 @@ describe 'Issue Boards add issue modal filtering', :js do
end end
def set_filter(type, text = '') def set_filter(type, text = '')
find('.add-issues-modal .filtered-search').native.send_keys("#{type}=#{text}") find('.add-issues-modal .filtered-search').native.send_keys("#{type}:=#{text}")
end end
def submit_filter def submit_filter
......
...@@ -28,14 +28,14 @@ describe 'Dashboard Issues filtering', :js do ...@@ -28,14 +28,14 @@ describe 'Dashboard Issues filtering', :js do
context 'filtering by milestone' do context 'filtering by milestone' do
it 'shows all issues with no milestone' do it 'shows all issues with no milestone' do
input_filtered_search("milestone=none") input_filtered_search("milestone:=none")
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_selector('.issue', count: 1) expect(page).to have_selector('.issue', count: 1)
end end
it 'shows all issues with the selected milestone' do it 'shows all issues with the selected milestone' do
input_filtered_search("milestone=%\"#{milestone.title}\"") input_filtered_search("milestone:=%\"#{milestone.title}\"")
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_selector('.issue', count: 1) expect(page).to have_selector('.issue', count: 1)
...@@ -63,7 +63,7 @@ describe 'Dashboard Issues filtering', :js do ...@@ -63,7 +63,7 @@ describe 'Dashboard Issues filtering', :js do
let!(:label_link) { create(:label_link, label: label, target: issue) } let!(:label_link) { create(:label_link, label: label, target: issue) }
it 'shows all issues with the selected label' do it 'shows all issues with the selected label' do
input_filtered_search("label=~#{label.title}") input_filtered_search("label:=~#{label.title}")
page.within 'ul.content-list' do page.within 'ul.content-list' do
expect(page).to have_content issue.title expect(page).to have_content issue.title
......
...@@ -30,7 +30,7 @@ RSpec.describe 'Dashboard Issues' do ...@@ -30,7 +30,7 @@ RSpec.describe 'Dashboard Issues' do
it 'shows issues when current user is author', :js do it 'shows issues when current user is author', :js do
reset_filters reset_filters
input_filtered_search("author=#{current_user.to_reference}") input_filtered_search("author:=#{current_user.to_reference}")
expect(page).to have_content(authored_issue.title) expect(page).to have_content(authored_issue.title)
expect(page).to have_content(authored_issue_on_public_project.title) expect(page).to have_content(authored_issue_on_public_project.title)
......
...@@ -107,7 +107,7 @@ describe 'Dashboard Merge Requests' do ...@@ -107,7 +107,7 @@ describe 'Dashboard Merge Requests' do
it 'shows authored merge requests', :js do it 'shows authored merge requests', :js do
reset_filters reset_filters
input_filtered_search("author=#{current_user.to_reference}") input_filtered_search("author:=#{current_user.to_reference}")
expect(page).to have_content(authored_merge_request.title) expect(page).to have_content(authored_merge_request.title)
expect(page).to have_content(authored_merge_request_from_fork.title) expect(page).to have_content(authored_merge_request_from_fork.title)
...@@ -120,7 +120,7 @@ describe 'Dashboard Merge Requests' do ...@@ -120,7 +120,7 @@ describe 'Dashboard Merge Requests' do
it 'shows labeled merge requests', :js do it 'shows labeled merge requests', :js do
reset_filters reset_filters
input_filtered_search("label=#{label.name}") input_filtered_search("label:=#{label.name}")
expect(page).to have_content(labeled_merge_request.title) expect(page).to have_content(labeled_merge_request.title)
......
...@@ -152,6 +152,61 @@ describe 'Dashboard Projects' do ...@@ -152,6 +152,61 @@ describe 'Dashboard Projects' do
end end
end end
describe 'with a pipeline', :clean_gitlab_redis_shared_state do
let(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.sha, ref: project.default_branch) }
before do
# Since the cache isn't updated when a new pipeline is created
# we need the pipeline to advance in the pipeline since the cache was created
# by visiting the login page.
pipeline.succeed
end
it 'shows that the last pipeline passed' do
visit dashboard_projects_path
page.within('.controls') do
expect(page).to have_xpath("//a[@href='#{pipelines_project_commit_path(project, project.commit, ref: pipeline.ref)}']")
expect(page).to have_css('.ci-status-link')
expect(page).to have_css('.ci-status-icon-success')
expect(page).to have_link('Pipeline: passed')
end
end
shared_examples 'hidden pipeline status' do
it 'does not show the pipeline status' do
visit dashboard_projects_path
page.within('.controls') do
expect(page).not_to have_xpath("//a[@href='#{pipelines_project_commit_path(project, project.commit, ref: pipeline.ref)}']")
expect(page).not_to have_css('.ci-status-link')
expect(page).not_to have_css('.ci-status-icon-success')
expect(page).not_to have_link('Pipeline: passed')
end
end
end
context 'guest user of project and project has private pipelines' do
let(:guest_user) { create(:user) }
before do
project.update(public_builds: false)
project.add_guest(guest_user)
sign_in(guest_user)
end
it_behaves_like 'hidden pipeline status'
end
context 'when dashboard_pipeline_status is disabled' do
before do
stub_feature_flags(dashboard_pipeline_status: false)
end
it_behaves_like 'hidden pipeline status'
end
end
context 'last push widget', :use_clean_rails_memory_store_caching do context 'last push widget', :use_clean_rails_memory_store_caching do
before do before do
event = create(:push_event, project: project, author: user) event = create(:push_event, project: project, author: user)
......
...@@ -48,7 +48,7 @@ describe 'Group issues page' do ...@@ -48,7 +48,7 @@ describe 'Group issues page' do
let(:user2) { user_outside_group } let(:user2) { user_outside_group }
it 'filters by only group users' do it 'filters by only group users' do
filtered_search.set('assignee=') filtered_search.set('assignee:=')
expect(find('#js-dropdown-assignee .filter-dropdown')).to have_content(user.name) expect(find('#js-dropdown-assignee .filter-dropdown')).to have_content(user.name)
expect(find('#js-dropdown-assignee .filter-dropdown')).not_to have_content(user2.name) expect(find('#js-dropdown-assignee .filter-dropdown')).not_to have_content(user2.name)
......
...@@ -52,7 +52,7 @@ describe 'Group merge requests page' do ...@@ -52,7 +52,7 @@ describe 'Group merge requests page' do
let(:user2) { user_outside_group } let(:user2) { user_outside_group }
it 'filters by assignee only group users' do it 'filters by assignee only group users' do
filtered_search.set('assignee=') filtered_search.set('assignee:=')
expect(find('#js-dropdown-assignee .filter-dropdown')).to have_content(user.name) expect(find('#js-dropdown-assignee .filter-dropdown')).to have_content(user.name)
expect(find('#js-dropdown-assignee .filter-dropdown')).not_to have_content(user2.name) expect(find('#js-dropdown-assignee .filter-dropdown')).not_to have_content(user2.name)
......
...@@ -20,13 +20,13 @@ describe 'Dropdown assignee', :js do ...@@ -20,13 +20,13 @@ describe 'Dropdown assignee', :js do
describe 'behavior' do describe 'behavior' do
it 'loads all the assignees when opened' do it 'loads all the assignees when opened' do
input_filtered_search('assignee=', submit: false, extra_space: false) input_filtered_search('assignee:=', submit: false, extra_space: false)
expect_filtered_search_dropdown_results(filter_dropdown, 2) expect_filtered_search_dropdown_results(filter_dropdown, 2)
end end
it 'shows current user at top of dropdown' do it 'shows current user at top of dropdown' do
input_filtered_search('assignee=', submit: false, extra_space: false) input_filtered_search('assignee:=', submit: false, extra_space: false)
expect(filter_dropdown.first('.filter-dropdown-item')).to have_content(user.name) expect(filter_dropdown.first('.filter-dropdown-item')).to have_content(user.name)
end end
...@@ -35,7 +35,7 @@ describe 'Dropdown assignee', :js do ...@@ -35,7 +35,7 @@ describe 'Dropdown assignee', :js do
describe 'selecting from dropdown without Ajax call' do describe 'selecting from dropdown without Ajax call' do
before do before do
Gitlab::Testing::RequestBlockerMiddleware.block_requests! Gitlab::Testing::RequestBlockerMiddleware.block_requests!
input_filtered_search('assignee=', submit: false, extra_space: false) input_filtered_search('assignee:=', submit: false, extra_space: false)
end end
after do after do
......
...@@ -20,13 +20,13 @@ describe 'Dropdown author', :js do ...@@ -20,13 +20,13 @@ describe 'Dropdown author', :js do
describe 'behavior' do describe 'behavior' do
it 'loads all the authors when opened' do it 'loads all the authors when opened' do
input_filtered_search('author=', submit: false, extra_space: false) input_filtered_search('author:=', submit: false, extra_space: false)
expect_filtered_search_dropdown_results(filter_dropdown, 2) expect_filtered_search_dropdown_results(filter_dropdown, 2)
end end
it 'shows current user at top of dropdown' do it 'shows current user at top of dropdown' do
input_filtered_search('author=', submit: false, extra_space: false) input_filtered_search('author:=', submit: false, extra_space: false)
expect(filter_dropdown.first('.filter-dropdown-item')).to have_content(user.name) expect(filter_dropdown.first('.filter-dropdown-item')).to have_content(user.name)
end end
...@@ -35,7 +35,7 @@ describe 'Dropdown author', :js do ...@@ -35,7 +35,7 @@ describe 'Dropdown author', :js do
describe 'selecting from dropdown without Ajax call' do describe 'selecting from dropdown without Ajax call' do
before do before do
Gitlab::Testing::RequestBlockerMiddleware.block_requests! Gitlab::Testing::RequestBlockerMiddleware.block_requests!
input_filtered_search('author=', submit: false, extra_space: false) input_filtered_search('author:=', submit: false, extra_space: false)
end end
after do after do
......
...@@ -27,14 +27,14 @@ describe 'Dropdown base', :js do ...@@ -27,14 +27,14 @@ describe 'Dropdown base', :js do
it 'shows loading indicator when opened' do it 'shows loading indicator when opened' do
slow_requests do slow_requests do
# We aren't using `input_filtered_search` because we want to see the loading indicator # We aren't using `input_filtered_search` because we want to see the loading indicator
filtered_search.set('assignee=') filtered_search.set('assignee:=')
expect(page).to have_css("#{js_dropdown_assignee} .filter-dropdown-loading", visible: true) expect(page).to have_css("#{js_dropdown_assignee} .filter-dropdown-loading", visible: true)
end end
end end
it 'hides loading indicator when loaded' do it 'hides loading indicator when loaded' do
input_filtered_search('assignee=', submit: false, extra_space: false) input_filtered_search('assignee:=', submit: false, extra_space: false)
expect(find(js_dropdown_assignee)).not_to have_css('.filter-dropdown-loading') expect(find(js_dropdown_assignee)).not_to have_css('.filter-dropdown-loading')
end end
...@@ -42,7 +42,7 @@ describe 'Dropdown base', :js do ...@@ -42,7 +42,7 @@ describe 'Dropdown base', :js do
describe 'caching requests' do describe 'caching requests' do
it 'caches requests after the first load' do it 'caches requests after the first load' do
input_filtered_search('assignee=', submit: false, extra_space: false) input_filtered_search('assignee:=', submit: false, extra_space: false)
initial_size = dropdown_assignee_size initial_size = dropdown_assignee_size
expect(initial_size).to be > 0 expect(initial_size).to be > 0
...@@ -50,7 +50,7 @@ describe 'Dropdown base', :js do ...@@ -50,7 +50,7 @@ describe 'Dropdown base', :js do
new_user = create(:user) new_user = create(:user)
project.add_maintainer(new_user) project.add_maintainer(new_user)
find('.filtered-search-box .clear-search').click find('.filtered-search-box .clear-search').click
input_filtered_search('assignee=', submit: false, extra_space: false) input_filtered_search('assignee:=', submit: false, extra_space: false)
expect(dropdown_assignee_size).to eq(initial_size) expect(dropdown_assignee_size).to eq(initial_size)
end end
......
...@@ -43,19 +43,19 @@ describe 'Dropdown emoji', :js do ...@@ -43,19 +43,19 @@ describe 'Dropdown emoji', :js do
describe 'behavior' do describe 'behavior' do
it 'opens when the search bar has my-reaction=' do it 'opens when the search bar has my-reaction=' do
filtered_search.set('my-reaction=') filtered_search.set('my-reaction:=')
expect(page).to have_css(js_dropdown_emoji, visible: true) expect(page).to have_css(js_dropdown_emoji, visible: true)
end end
it 'loads all the emojis when opened' do it 'loads all the emojis when opened' do
input_filtered_search('my-reaction=', submit: false, extra_space: false) input_filtered_search('my-reaction:=', submit: false, extra_space: false)
expect_filtered_search_dropdown_results(filter_dropdown, 3) expect_filtered_search_dropdown_results(filter_dropdown, 3)
end end
it 'shows the most populated emoji at top of dropdown' do it 'shows the most populated emoji at top of dropdown' do
input_filtered_search('my-reaction=', submit: false, extra_space: false) input_filtered_search('my-reaction:=', submit: false, extra_space: false)
expect(first("#{js_dropdown_emoji} .filter-dropdown li")).to have_content(award_emoji_star.name) expect(first("#{js_dropdown_emoji} .filter-dropdown li")).to have_content(award_emoji_star.name)
end end
......
...@@ -21,7 +21,7 @@ describe 'Dropdown label', :js do ...@@ -21,7 +21,7 @@ describe 'Dropdown label', :js do
describe 'behavior' do describe 'behavior' do
it 'loads all the labels when opened' do it 'loads all the labels when opened' do
create(:label, project: project, title: 'bug-label') create(:label, project: project, title: 'bug-label')
filtered_search.set('label=') filtered_search.set('label:=')
expect_filtered_search_dropdown_results(filter_dropdown, 1) expect_filtered_search_dropdown_results(filter_dropdown, 1)
end end
......
...@@ -23,7 +23,7 @@ describe 'Dropdown milestone', :js do ...@@ -23,7 +23,7 @@ describe 'Dropdown milestone', :js do
describe 'behavior' do describe 'behavior' do
before do before do
filtered_search.set('milestone=') filtered_search.set('milestone:=')
end end
it 'loads all the milestones when opened' do it 'loads all the milestones when opened' do
......
...@@ -23,7 +23,7 @@ describe 'Dropdown release', :js do ...@@ -23,7 +23,7 @@ describe 'Dropdown release', :js do
describe 'behavior' do describe 'behavior' do
before do before do
filtered_search.set('release=') filtered_search.set('release:=')
end end
it 'loads all the releases when opened' do it 'loads all the releases when opened' do
......
...@@ -36,7 +36,7 @@ describe 'Visual tokens', :js do ...@@ -36,7 +36,7 @@ describe 'Visual tokens', :js do
describe 'editing a single token' do describe 'editing a single token' do
before do before do
input_filtered_search('author=@root assignee=none', submit: false) input_filtered_search('author:=@root assignee:=none', submit: false)
first('.tokens-container .filtered-search-token').click first('.tokens-container .filtered-search-token').click
wait_for_requests wait_for_requests
end end
...@@ -77,7 +77,7 @@ describe 'Visual tokens', :js do ...@@ -77,7 +77,7 @@ describe 'Visual tokens', :js do
describe 'editing multiple tokens' do describe 'editing multiple tokens' do
before do before do
input_filtered_search('author=@root assignee=none', submit: false) input_filtered_search('author:=@root assignee:=none', submit: false)
first('.tokens-container .filtered-search-token').click first('.tokens-container .filtered-search-token').click
end end
...@@ -93,7 +93,7 @@ describe 'Visual tokens', :js do ...@@ -93,7 +93,7 @@ describe 'Visual tokens', :js do
describe 'editing a search term while editing another filter token' do describe 'editing a search term while editing another filter token' do
before do before do
input_filtered_search('foo assignee=', submit: false) input_filtered_search('foo assignee:=', submit: false)
first('.tokens-container .filtered-search-term').click first('.tokens-container .filtered-search-term').click
end end
...@@ -112,7 +112,7 @@ describe 'Visual tokens', :js do ...@@ -112,7 +112,7 @@ describe 'Visual tokens', :js do
describe 'add new token after editing existing token' do describe 'add new token after editing existing token' do
before do before do
input_filtered_search('author=@root assignee=none', submit: false) input_filtered_search('author:=@root assignee:=none', submit: false)
first('.tokens-container .filtered-search-token').double_click first('.tokens-container .filtered-search-token').double_click
filtered_search.send_keys(' ') filtered_search.send_keys(' ')
end end
...@@ -123,7 +123,7 @@ describe 'Visual tokens', :js do ...@@ -123,7 +123,7 @@ describe 'Visual tokens', :js do
end end
it 'opens token dropdown' do it 'opens token dropdown' do
filtered_search.send_keys('author=') filtered_search.send_keys('author:=')
expect(page).to have_css('#js-dropdown-author', visible: true) expect(page).to have_css('#js-dropdown-author', visible: true)
end end
...@@ -131,7 +131,7 @@ describe 'Visual tokens', :js do ...@@ -131,7 +131,7 @@ describe 'Visual tokens', :js do
describe 'visual tokens' do describe 'visual tokens' do
it 'creates visual token' do it 'creates visual token' do
filtered_search.send_keys('author=@thomas ') filtered_search.send_keys('author:=@thomas ')
token = page.all('.tokens-container .filtered-search-token')[1] token = page.all('.tokens-container .filtered-search-token')[1]
expect(token.find('.name').text).to eq('Author') expect(token.find('.name').text).to eq('Author')
...@@ -140,7 +140,7 @@ describe 'Visual tokens', :js do ...@@ -140,7 +140,7 @@ describe 'Visual tokens', :js do
end end
it 'does not tokenize incomplete token' do it 'does not tokenize incomplete token' do
filtered_search.send_keys('author=') filtered_search.send_keys('author:=')
find('body').click find('body').click
token = page.all('.tokens-container .js-visual-token')[1] token = page.all('.tokens-container .js-visual-token')[1]
...@@ -152,7 +152,7 @@ describe 'Visual tokens', :js do ...@@ -152,7 +152,7 @@ describe 'Visual tokens', :js do
describe 'search using incomplete visual tokens' do describe 'search using incomplete visual tokens' do
before do before do
input_filtered_search('author=@root assignee=none', extra_space: false) input_filtered_search('author:=@root assignee:=none', extra_space: false)
end end
it 'tokenizes the search term to complete visual token' do it 'tokenizes the search term to complete visual token' do
......
...@@ -23,7 +23,7 @@ describe 'Merge Requests > Filters generic behavior', :js do ...@@ -23,7 +23,7 @@ describe 'Merge Requests > Filters generic behavior', :js do
context 'when filtered by a label' do context 'when filtered by a label' do
before do before do
input_filtered_search('label=~bug') input_filtered_search('label:=~bug')
end end
describe 'state tabs' do describe 'state tabs' do
......
...@@ -18,7 +18,7 @@ describe 'Merge Requests > User filters by assignees', :js do ...@@ -18,7 +18,7 @@ describe 'Merge Requests > User filters by assignees', :js do
context 'filtering by assignee:none' do context 'filtering by assignee:none' do
it 'applies the filter' do it 'applies the filter' do
input_filtered_search('assignee=none') input_filtered_search('assignee:=none')
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).not_to have_content 'Bugfix1' expect(page).not_to have_content 'Bugfix1'
...@@ -28,7 +28,7 @@ describe 'Merge Requests > User filters by assignees', :js do ...@@ -28,7 +28,7 @@ describe 'Merge Requests > User filters by assignees', :js do
context 'filtering by assignee=@username' do context 'filtering by assignee=@username' do
it 'applies the filter' do it 'applies the filter' do
input_filtered_search("assignee=@#{user.username}") input_filtered_search("assignee:=@#{user.username}")
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_content 'Bugfix1' expect(page).to have_content 'Bugfix1'
......
...@@ -22,7 +22,7 @@ describe 'Merge Requests > User filters by labels', :js do ...@@ -22,7 +22,7 @@ describe 'Merge Requests > User filters by labels', :js do
context 'filtering by label:none' do context 'filtering by label:none' do
it 'applies the filter' do it 'applies the filter' do
input_filtered_search('label=none') input_filtered_search('label:=none')
expect(page).to have_issuable_counts(open: 0, closed: 0, all: 0) expect(page).to have_issuable_counts(open: 0, closed: 0, all: 0)
expect(page).not_to have_content 'Bugfix1' expect(page).not_to have_content 'Bugfix1'
...@@ -32,7 +32,7 @@ describe 'Merge Requests > User filters by labels', :js do ...@@ -32,7 +32,7 @@ describe 'Merge Requests > User filters by labels', :js do
context 'filtering by label:~enhancement' do context 'filtering by label:~enhancement' do
it 'applies the filter' do it 'applies the filter' do
input_filtered_search('label=~enhancement') input_filtered_search('label:=~enhancement')
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_content 'Bugfix2' expect(page).to have_content 'Bugfix2'
...@@ -42,7 +42,7 @@ describe 'Merge Requests > User filters by labels', :js do ...@@ -42,7 +42,7 @@ describe 'Merge Requests > User filters by labels', :js do
context 'filtering by label:~enhancement and label:~bug' do context 'filtering by label:~enhancement and label:~bug' do
it 'applies the filters' do it 'applies the filters' do
input_filtered_search('label=~bug label=~enhancement') input_filtered_search('label:=~bug label:=~enhancement')
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_content 'Bugfix2' expect(page).to have_content 'Bugfix2'
......
...@@ -18,14 +18,14 @@ describe 'Merge Requests > User filters by milestones', :js do ...@@ -18,14 +18,14 @@ describe 'Merge Requests > User filters by milestones', :js do
end end
it 'filters by no milestone' do it 'filters by no milestone' do
input_filtered_search('milestone=none') input_filtered_search('milestone:=none')
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_css('.merge-request', count: 1) expect(page).to have_css('.merge-request', count: 1)
end end
it 'filters by a specific milestone' do it 'filters by a specific milestone' do
input_filtered_search("milestone=%'#{milestone.title}'") input_filtered_search("milestone:=%'#{milestone.title}'")
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_css('.merge-request', count: 1) expect(page).to have_css('.merge-request', count: 1)
...@@ -33,7 +33,7 @@ describe 'Merge Requests > User filters by milestones', :js do ...@@ -33,7 +33,7 @@ describe 'Merge Requests > User filters by milestones', :js do
describe 'filters by upcoming milestone' do describe 'filters by upcoming milestone' do
it 'does not show merge requests with no expiry' do it 'does not show merge requests with no expiry' do
input_filtered_search('milestone=upcoming') input_filtered_search('milestone:=upcoming')
expect(page).to have_issuable_counts(open: 0, closed: 0, all: 0) expect(page).to have_issuable_counts(open: 0, closed: 0, all: 0)
expect(page).to have_css('.merge-request', count: 0) expect(page).to have_css('.merge-request', count: 0)
...@@ -43,7 +43,7 @@ describe 'Merge Requests > User filters by milestones', :js do ...@@ -43,7 +43,7 @@ describe 'Merge Requests > User filters by milestones', :js do
let(:milestone) { create(:milestone, project: project, due_date: Date.tomorrow) } let(:milestone) { create(:milestone, project: project, due_date: Date.tomorrow) }
it 'shows merge requests' do it 'shows merge requests' do
input_filtered_search('milestone=upcoming') input_filtered_search('milestone:=upcoming')
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_css('.merge-request', count: 1) expect(page).to have_css('.merge-request', count: 1)
...@@ -54,7 +54,7 @@ describe 'Merge Requests > User filters by milestones', :js do ...@@ -54,7 +54,7 @@ describe 'Merge Requests > User filters by milestones', :js do
let(:milestone) { create(:milestone, project: project, due_date: Date.yesterday) } let(:milestone) { create(:milestone, project: project, due_date: Date.yesterday) }
it 'does not show any merge requests' do it 'does not show any merge requests' do
input_filtered_search('milestone=upcoming') input_filtered_search('milestone:=upcoming')
expect(page).to have_issuable_counts(open: 0, closed: 0, all: 0) expect(page).to have_issuable_counts(open: 0, closed: 0, all: 0)
expect(page).to have_css('.merge-request', count: 0) expect(page).to have_css('.merge-request', count: 0)
......
...@@ -20,7 +20,7 @@ describe 'Merge requests > User filters by multiple criteria', :js do ...@@ -20,7 +20,7 @@ describe 'Merge requests > User filters by multiple criteria', :js do
describe 'filtering by label:~"Won\'t fix" and assignee:~bug' do describe 'filtering by label:~"Won\'t fix" and assignee:~bug' do
it 'applies the filters' do it 'applies the filters' do
input_filtered_search("label=~\"Won't fix\" assignee=@#{user.username}") input_filtered_search("label:=~\"Won't fix\" assignee:=@#{user.username}")
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_content 'Bugfix2' expect(page).to have_content 'Bugfix2'
...@@ -30,7 +30,7 @@ describe 'Merge requests > User filters by multiple criteria', :js do ...@@ -30,7 +30,7 @@ describe 'Merge requests > User filters by multiple criteria', :js do
describe 'filtering by text, author, assignee, milestone, and label' do describe 'filtering by text, author, assignee, milestone, and label' do
it 'filters by text, author, assignee, milestone, and label' do it 'filters by text, author, assignee, milestone, and label' do
input_filtered_search_keys("author=@#{user.username} assignee=@#{user.username} milestone=%\"v1.1\" label=~\"Won't fix\" Bug") input_filtered_search_keys("author:=@#{user.username} assignee:=@#{user.username} milestone:=%\"v1.1\" label:=~\"Won't fix\" Bug")
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_content 'Bugfix2' expect(page).to have_content 'Bugfix2'
......
...@@ -17,7 +17,7 @@ describe 'Merge Requests > User filters by target branch', :js do ...@@ -17,7 +17,7 @@ describe 'Merge Requests > User filters by target branch', :js do
context 'filtering by target-branch:master' do context 'filtering by target-branch:master' do
it 'applies the filter' do it 'applies the filter' do
input_filtered_search('target-branch=master') input_filtered_search('target-branch:=master')
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_content mr1.title expect(page).to have_content mr1.title
...@@ -27,7 +27,7 @@ describe 'Merge Requests > User filters by target branch', :js do ...@@ -27,7 +27,7 @@ describe 'Merge Requests > User filters by target branch', :js do
context 'filtering by target-branch:merged-target' do context 'filtering by target-branch:merged-target' do
it 'applies the filter' do it 'applies the filter' do
input_filtered_search('target-branch=merged-target') input_filtered_search('target-branch:=merged-target')
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).not_to have_content mr1.title expect(page).not_to have_content mr1.title
...@@ -37,7 +37,7 @@ describe 'Merge Requests > User filters by target branch', :js do ...@@ -37,7 +37,7 @@ describe 'Merge Requests > User filters by target branch', :js do
context 'filtering by target-branch:feature' do context 'filtering by target-branch:feature' do
it 'applies the filter' do it 'applies the filter' do
input_filtered_search('target-branch=feature') input_filtered_search('target-branch:=feature')
expect(page).to have_issuable_counts(open: 0, closed: 0, all: 0) expect(page).to have_issuable_counts(open: 0, closed: 0, all: 0)
expect(page).not_to have_content mr1.title expect(page).not_to have_content mr1.title
......
# frozen_string_literal: true
require 'spec_helper'
describe 'User follows pipeline suggest nudge spec when feature is enabled', :js do
let(:user) { create(:user, :admin) }
let(:project) { create(:project, :empty_repo) }
describe 'viewing the new blob page' do
before do
stub_feature_flags(suggest_pipeline: true)
sign_in(user)
end
context 'when the page is loaded from the link using the suggest_gitlab_ci_yml param' do
before do
visit namespace_project_new_blob_path(namespace_id: project.namespace, project_id: project, id: 'master', suggest_gitlab_ci_yml: 'true')
end
it 'pre-fills .gitlab-ci.yml for file name' do
file_name = page.find_by_id('file_name')
expect(file_name.value).to have_content('.gitlab-ci.yml')
end
it 'chooses the .gitlab-ci.yml Template Type' do
template_type = page.find(:css, '.template-type-selector .dropdown-toggle-text')
expect(template_type.text).to have_content('.gitlab-ci.yml')
end
it 'displays suggest_gitlab_ci_yml popover' do
popover_selector = '.suggest-gitlab-ci-yml'
expect(page).to have_css(popover_selector, visible: true)
page.within(popover_selector) do
expect(page).to have_content('1/2: Choose a template')
end
end
end
context 'when the page is visited without the param' do
before do
visit namespace_project_new_blob_path(namespace_id: project.namespace, project_id: project, id: 'master')
end
it 'does not pre-fill .gitlab-ci.yml for file name' do
file_name = page.find_by_id('file_name')
expect(file_name.value).not_to have_content('.gitlab-ci.yml')
end
it 'does not choose the .gitlab-ci.yml Template Type' do
template_type = page.find(:css, '.template-type-selector .dropdown-toggle-text')
expect(template_type.text).to have_content('Select a template type')
end
it 'does not display suggest_gitlab_ci_yml popover' do
popover_selector = '.b-popover.suggest-gitlab-ci-yml'
expect(page).not_to have_css(popover_selector, visible: true)
end
end
end
end
...@@ -6,22 +6,27 @@ describe 'User edits Release', :js do ...@@ -6,22 +6,27 @@ describe 'User edits Release', :js do
let_it_be(:project) { create(:project, :repository) } let_it_be(:project) { create(:project, :repository) }
let_it_be(:release) { create(:release, project: project, name: 'The first release' ) } let_it_be(:release) { create(:release, project: project, name: 'The first release' ) }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let(:show_feature_flag) { true }
before do before do
stub_feature_flags(release_show_page: show_feature_flag)
project.add_developer(user) project.add_developer(user)
gitlab_sign_in(user) gitlab_sign_in(user)
visit edit_project_release_path(project, release) visit edit_project_release_path(project, release)
wait_for_requests
end end
def fill_out_form_and_click(button_to_click) def fill_out_form_and_click(button_to_click)
fill_in 'Release title', with: 'Updated Release title' fill_in 'Release title', with: 'Updated Release title'
fill_in 'Release notes', with: 'Updated Release notes' fill_in 'Release notes', with: 'Updated Release notes'
click_button button_to_click click_link_or_button button_to_click
wait_for_requests wait_for_all_requests
end end
it 'renders the breadcrumbs' do it 'renders the breadcrumbs' do
...@@ -42,31 +47,66 @@ describe 'User edits Release', :js do ...@@ -42,31 +47,66 @@ describe 'User edits Release', :js do
expect(find_field('Release notes').value).to eq(release.description) expect(find_field('Release notes').value).to eq(release.description)
expect(page).to have_button('Save changes') expect(page).to have_button('Save changes')
expect(page).to have_button('Cancel') expect(page).to have_link('Cancel')
end end
it 'redirects to the main Releases page without updating the Release when "Cancel" is clicked' do it 'does not update the Release when "Cancel" is clicked' do
original_name = release.name original_name = release.name
original_description = release.description original_description = release.description
fill_out_form_and_click 'Cancel' fill_out_form_and_click 'Cancel'
expect(current_path).to eq(project_releases_path(project))
release.reload release.reload
expect(release.name).to eq(original_name) expect(release.name).to eq(original_name)
expect(release.description).to eq(original_description) expect(release.description).to eq(original_description)
end end
it 'updates the Release and redirects to the main Releases page when "Save changes" is clicked' do it 'updates the Release when "Save changes" is clicked' do
fill_out_form_and_click 'Save changes' fill_out_form_and_click 'Save changes'
expect(current_path).to eq(project_releases_path(project))
release.reload release.reload
expect(release.name).to eq('Updated Release title') expect(release.name).to eq('Updated Release title')
expect(release.description).to eq('Updated Release notes') expect(release.description).to eq('Updated Release notes')
end end
context 'when the release_show_page feature flag is disabled' do
let(:show_feature_flag) { false }
it 'redirects to the main Releases page when "Cancel" is clicked' do
fill_out_form_and_click 'Cancel'
expect(page).to have_current_path(project_releases_path(project))
end
it 'redirects to the main Releases page when "Save changes" is clicked' do
fill_out_form_and_click 'Save changes'
expect(page).to have_current_path(project_releases_path(project))
end
end
context 'when the release_show_page feature flag is enabled' do
it 'redirects to the previous page when "Cancel" is clicked when the url includes a back_url query parameter' do
back_path = project_releases_path(project, params: { page: 2 })
visit edit_project_release_path(project, release, params: { back_url: back_path })
fill_out_form_and_click 'Cancel'
expect(page).to have_current_path(back_path)
end
it 'redirects to the main Releases page when "Cancel" is clicked when the url does not include a back_url query parameter' do
fill_out_form_and_click 'Cancel'
expect(page).to have_current_path(project_releases_path(project))
end
it 'redirects to the dedicated Release page when "Save changes" is clicked' do
fill_out_form_and_click 'Save changes'
expect(page).to have_current_path(project_release_path(project, release))
end
end
end end
# frozen_string_literal: true
require 'spec_helper'
describe 'User views Release', :js do
let(:project) { create(:project, :repository) }
let(:release) { create(:release, project: project, name: 'The first release' ) }
let(:user) { create(:user) }
before do
project.add_developer(user)
gitlab_sign_in(user)
visit project_release_path(project, release)
end
it 'renders the breadcrumbs' do
within('.breadcrumbs') do
expect(page).to have_content("#{project.creator.name} #{project.name} Releases #{release.name}")
expect(page).to have_link(project.creator.name, href: user_path(project.creator))
expect(page).to have_link(project.name, href: project_path(project))
expect(page).to have_link('Releases', href: project_releases_path(project))
expect(page).to have_link(release.name, href: project_release_path(project, release))
end
end
it 'renders the release details' do
within('.release-block') do
expect(page).to have_content(release.name)
expect(page).to have_content(release.tag)
expect(page).to have_content(release.commit.short_id)
expect(page).to have_content(release.description)
end
end
end
import { shallowMount } from '@vue/test-utils';
import Popover from '~/blob/suggest_gitlab_ci_yml/components/popover.vue';
import Cookies from 'js-cookie';
const popoverTarget = 'gitlab-ci-yml-selector';
const dismissKey = 'suggest_gitlab_ci_yml_99';
describe('Suggest gitlab-ci.yml Popover', () => {
let wrapper;
function createWrapper() {
wrapper = shallowMount(Popover, {
propsData: {
target: popoverTarget,
cssClass: 'js-class',
dismissKey,
},
});
}
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('when no dismiss cookie is set', () => {
beforeEach(() => {
createWrapper();
});
it('sets popoverDismissed to false', () => {
expect(wrapper.vm.popoverDismissed).toEqual(false);
});
});
describe('when the dismiss cookie is set', () => {
beforeEach(() => {
Cookies.set(dismissKey, true);
createWrapper();
});
it('sets popoverDismissed to true', () => {
expect(wrapper.vm.popoverDismissed).toEqual(true);
});
});
});
...@@ -6,56 +6,75 @@ jest.mock('~/lib/utils/icon_utils', () => ({ ...@@ -6,56 +6,75 @@ jest.mock('~/lib/utils/icon_utils', () => ({
getSvgIconPathContent: jest.fn().mockResolvedValue('mockSvgPathContent'), getSvgIconPathContent: jest.fn().mockResolvedValue('mockSvgPathContent'),
})); }));
const yAxisName = 'Y-axis mock name';
const yAxisFormat = 'bytes';
const yAxisPrecistion = 3;
const dataValues = [
[1495700554.925, '8.0390625'],
[1495700614.925, '8.0390625'],
[1495700674.925, '8.0390625'],
];
describe('Column component', () => { describe('Column component', () => {
let columnChart; let wrapper;
const findChart = () => wrapper.find(GlColumnChart);
const chartProps = prop => findChart().props(prop);
beforeEach(() => { beforeEach(() => {
columnChart = shallowMount(ColumnChart, { wrapper = shallowMount(ColumnChart, {
propsData: { propsData: {
graphData: { graphData: {
yAxis: {
name: yAxisName,
format: yAxisFormat,
precision: yAxisPrecistion,
},
metrics: [ metrics: [
{ {
x_label: 'Time',
y_label: 'Usage',
result: [ result: [
{ {
metric: {}, metric: {},
values: [ values: dataValues,
[1495700554.925, '8.0390625'],
[1495700614.925, '8.0390625'],
[1495700674.925, '8.0390625'],
],
}, },
], ],
}, },
], ],
}, },
containerWidth: 100,
}, },
}); });
}); });
afterEach(() => { afterEach(() => {
columnChart.destroy(); wrapper.destroy();
}); });
describe('wrapped components', () => { describe('wrapped components', () => {
describe('GitLab UI column chart', () => { describe('GitLab UI column chart', () => {
let glColumnChart; it('is a Vue instance', () => {
expect(findChart().isVueInstance()).toBe(true);
});
beforeEach(() => { it('receives data properties needed for proper chart render', () => {
glColumnChart = columnChart.find(GlColumnChart); expect(chartProps('data').values).toEqual(dataValues);
}); });
it('is a Vue instance', () => { it('passes the y axis name correctly', () => {
expect(glColumnChart.isVueInstance()).toBe(true); expect(chartProps('yAxisTitle')).toBe(yAxisName);
}); });
it('receives data properties needed for proper chart render', () => { it('passes the y axis configuration correctly', () => {
const props = glColumnChart.props(); expect(chartProps('option').yAxis).toMatchObject({
name: yAxisName,
axisLabel: {
formatter: expect.any(Function),
},
scale: false,
});
});
expect(props.data).toBe(columnChart.vm.chartData); it('passes a dataZoom configuration', () => {
expect(props.option).toBe(columnChart.vm.chartOptions); expect(chartProps('option').dataZoom).toBeDefined();
}); });
}); });
}); });
......
...@@ -544,6 +544,12 @@ export const dashboardGitResponse = [ ...@@ -544,6 +544,12 @@ export const dashboardGitResponse = [
...customDashboardsData, ...customDashboardsData,
]; ];
export const mockDashboardsErrorResponse = {
all_dashboards: customDashboardsData,
message: "Each 'panel_group' must define an array :panels",
status: 'error',
};
export const graphDataPrometheusQuery = { export const graphDataPrometheusQuery = {
title: 'Super Chart A2', title: 'Super Chart A2',
type: 'single-stat', type: 'single-stat',
......
...@@ -30,6 +30,7 @@ import { ...@@ -30,6 +30,7 @@ import {
metricsDashboardResponse, metricsDashboardResponse,
metricsDashboardViewModel, metricsDashboardViewModel,
dashboardGitResponse, dashboardGitResponse,
mockDashboardsErrorResponse,
} from '../mock_data'; } from '../mock_data';
jest.mock('~/flash'); jest.mock('~/flash');
...@@ -257,9 +258,11 @@ describe('Monitoring store actions', () => { ...@@ -257,9 +258,11 @@ describe('Monitoring store actions', () => {
describe('fetchDashboard', () => { describe('fetchDashboard', () => {
let dispatch; let dispatch;
let state; let state;
let commit;
const response = metricsDashboardResponse; const response = metricsDashboardResponse;
beforeEach(() => { beforeEach(() => {
dispatch = jest.fn(); dispatch = jest.fn();
commit = jest.fn();
state = storeState(); state = storeState();
state.dashboardEndpoint = '/dashboard'; state.dashboardEndpoint = '/dashboard';
}); });
...@@ -270,6 +273,7 @@ describe('Monitoring store actions', () => { ...@@ -270,6 +273,7 @@ describe('Monitoring store actions', () => {
fetchDashboard( fetchDashboard(
{ {
state, state,
commit,
dispatch, dispatch,
}, },
params, params,
...@@ -287,19 +291,21 @@ describe('Monitoring store actions', () => { ...@@ -287,19 +291,21 @@ describe('Monitoring store actions', () => {
describe('on failure', () => { describe('on failure', () => {
let result; let result;
let errorResponse;
beforeEach(() => { beforeEach(() => {
const params = {}; const params = {};
result = () => { result = () => {
mock.onGet(state.dashboardEndpoint).replyOnce(500, errorResponse); mock.onGet(state.dashboardEndpoint).replyOnce(500, mockDashboardsErrorResponse);
return fetchDashboard({ state, dispatch }, params); return fetchDashboard({ state, commit, dispatch }, params);
}; };
}); });
it('dispatches a failure action', done => { it('dispatches a failure action', done => {
errorResponse = {};
result() result()
.then(() => { .then(() => {
expect(commit).toHaveBeenCalledWith(
types.SET_ALL_DASHBOARDS,
mockDashboardsErrorResponse.all_dashboards,
);
expect(dispatch).toHaveBeenCalledWith( expect(dispatch).toHaveBeenCalledWith(
'receiveMetricsDashboardFailure', 'receiveMetricsDashboardFailure',
new Error('Request failed with status code 500'), new Error('Request failed with status code 500'),
...@@ -311,15 +317,15 @@ describe('Monitoring store actions', () => { ...@@ -311,15 +317,15 @@ describe('Monitoring store actions', () => {
}); });
it('dispatches a failure action when a message is returned', done => { it('dispatches a failure action when a message is returned', done => {
const message = 'Something went wrong with Prometheus!';
errorResponse = { message };
result() result()
.then(() => { .then(() => {
expect(dispatch).toHaveBeenCalledWith( expect(dispatch).toHaveBeenCalledWith(
'receiveMetricsDashboardFailure', 'receiveMetricsDashboardFailure',
new Error('Request failed with status code 500'), new Error('Request failed with status code 500'),
); );
expect(createFlash).toHaveBeenCalledWith(expect.stringContaining(message)); expect(createFlash).toHaveBeenCalledWith(
expect.stringContaining(mockDashboardsErrorResponse.message),
);
done(); done();
}) })
.catch(done.fail); .catch(done.fail);
......
import Vuex from 'vuex'; import Vuex from 'vuex';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import ReleaseEditApp from '~/releases/components/app_edit.vue'; import ReleaseEditApp from '~/releases/components/app_edit.vue';
import { release } from '../mock_data'; import { release as originalRelease } from '../mock_data';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import * as commonUtils from '~/lib/utils/common_utils';
import { BACK_URL_PARAM } from '~/releases/constants';
describe('Release edit component', () => { describe('Release edit component', () => {
let wrapper; let wrapper;
let releaseClone; let release;
let actions; let actions;
let state; let state;
beforeEach(() => { const factory = () => {
gon.api_version = 'v4';
releaseClone = convertObjectPropsToCamelCase(release, { deep: true });
state = { state = {
release: releaseClone, release,
markdownDocsPath: 'path/to/markdown/docs', markdownDocsPath: 'path/to/markdown/docs',
updateReleaseApiDocsPath: 'path/to/update/release/api/docs', updateReleaseApiDocsPath: 'path/to/update/release/api/docs',
releasesPagePath: 'path/to/releases/page',
}; };
actions = { actions = {
fetchRelease: jest.fn(), fetchRelease: jest.fn(),
updateRelease: jest.fn(), updateRelease: jest.fn(),
navigateToReleasesPage: jest.fn(),
}; };
const store = new Vuex.Store({ const store = new Vuex.Store({
...@@ -40,8 +37,22 @@ describe('Release edit component', () => { ...@@ -40,8 +37,22 @@ describe('Release edit component', () => {
wrapper = mount(ReleaseEditApp, { wrapper = mount(ReleaseEditApp, {
store, store,
}); });
};
beforeEach(() => {
gon.api_version = 'v4';
release = commonUtils.convertObjectPropsToCamelCase(originalRelease, { deep: true });
});
return wrapper.vm.$nextTick(); afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe(`basic functionality tests: all tests unrelated to the "${BACK_URL_PARAM}" parameter`, () => {
beforeEach(() => {
factory();
}); });
it('calls fetchRelease when the component is created', () => { it('calls fetchRelease when the component is created', () => {
...@@ -55,7 +66,7 @@ describe('Release edit component', () => { ...@@ -55,7 +66,7 @@ describe('Release edit component', () => {
}); });
it('renders the correct tag name in the "Tag name" field', () => { it('renders the correct tag name in the "Tag name" field', () => {
expect(wrapper.find('#git-ref').element.value).toBe(releaseClone.tagName); expect(wrapper.find('#git-ref').element.value).toBe(release.tagName);
}); });
it('renders the correct help text under the "Tag name" field', () => { it('renders the correct help text under the "Tag name" field', () => {
...@@ -67,18 +78,21 @@ describe('Release edit component', () => { ...@@ -67,18 +78,21 @@ describe('Release edit component', () => {
'Changing a Release tag is only supported via Releases API. More information', 'Changing a Release tag is only supported via Releases API. More information',
); );
expect(helperTextLink.text()).toBe('More information'); expect(helperTextLink.text()).toBe('More information');
expect(helperTextLinkAttrs.href).toBe(state.updateReleaseApiDocsPath); expect(helperTextLinkAttrs).toEqual(
expect(helperTextLinkAttrs.rel).toContain('noopener'); expect.objectContaining({
expect(helperTextLinkAttrs.rel).toContain('noreferrer'); href: state.updateReleaseApiDocsPath,
expect(helperTextLinkAttrs.target).toBe('_blank'); rel: 'noopener noreferrer',
target: '_blank',
}),
);
}); });
it('renders the correct release title in the "Release title" field', () => { it('renders the correct release title in the "Release title" field', () => {
expect(wrapper.find('#release-title').element.value).toBe(releaseClone.name); expect(wrapper.find('#release-title').element.value).toBe(release.name);
}); });
it('renders the release notes in the "Release notes" textarea', () => { it('renders the release notes in the "Release notes" textarea', () => {
expect(wrapper.find('#release-notes').element.value).toBe(releaseClone.description); expect(wrapper.find('#release-notes').element.value).toBe(release.description);
}); });
it('renders the "Save changes" button as type="submit"', () => { it('renders the "Save changes" button as type="submit"', () => {
...@@ -89,9 +103,33 @@ describe('Release edit component', () => { ...@@ -89,9 +103,33 @@ describe('Release edit component', () => {
wrapper.find('form').trigger('submit'); wrapper.find('form').trigger('submit');
expect(actions.updateRelease).toHaveBeenCalledTimes(1); expect(actions.updateRelease).toHaveBeenCalledTimes(1);
}); });
});
describe(`when the URL does not contain a "${BACK_URL_PARAM}" parameter`, () => {
beforeEach(() => {
factory();
});
it(`renders a "Cancel" button with an href pointing to "${BACK_URL_PARAM}"`, () => {
const cancelButton = wrapper.find('.js-cancel-button');
expect(cancelButton.attributes().href).toBe(state.releasesPagePath);
});
});
it('calls navigateToReleasesPage when the "Cancel" button is clicked', () => { describe(`when the URL contains a "${BACK_URL_PARAM}" parameter`, () => {
wrapper.find('.js-cancel-button').vm.$emit('click'); const backUrl = 'https://example.gitlab.com/back/url';
expect(actions.navigateToReleasesPage).toHaveBeenCalledTimes(1);
beforeEach(() => {
commonUtils.getParameterByName = jest
.fn()
.mockImplementation(paramToGet => ({ [BACK_URL_PARAM]: backUrl }[paramToGet]));
factory();
});
it('renders a "Cancel" button with an href pointing to the main Releases page', () => {
const cancelButton = wrapper.find('.js-cancel-button');
expect(cancelButton.attributes().href).toBe(backUrl);
});
}); });
}); });
import Vuex from 'vuex';
import { shallowMount } from '@vue/test-utils';
import ReleaseShowApp from '~/releases/components/app_show.vue';
import { release as originalRelease } from '../mock_data';
import { GlSkeletonLoading } from '@gitlab/ui';
import ReleaseBlock from '~/releases/components/release_block.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
describe('Release show component', () => {
let wrapper;
let release;
let actions;
beforeEach(() => {
release = convertObjectPropsToCamelCase(originalRelease);
});
const factory = state => {
actions = {
fetchRelease: jest.fn(),
};
const store = new Vuex.Store({
modules: {
detail: {
namespaced: true,
actions,
state,
},
},
});
wrapper = shallowMount(ReleaseShowApp, { store });
};
const findLoadingSkeleton = () => wrapper.find(GlSkeletonLoading);
const findReleaseBlock = () => wrapper.find(ReleaseBlock);
it('calls fetchRelease when the component is created', () => {
factory({ release });
expect(actions.fetchRelease).toHaveBeenCalledTimes(1);
});
it('shows a loading skeleton and hides the release block while the API call is in progress', () => {
factory({ isFetchingRelease: true });
expect(findLoadingSkeleton().exists()).toBe(true);
expect(findReleaseBlock().exists()).toBe(false);
});
it('hides the loading skeleton and shows the release block when the API call finishes successfully', () => {
factory({ isFetchingRelease: false });
expect(findLoadingSkeleton().exists()).toBe(false);
expect(findReleaseBlock().exists()).toBe(true);
});
it('hides both the loading skeleton and the release block when the API call fails', () => {
factory({ fetchError: new Error('Uh oh') });
expect(findLoadingSkeleton().exists()).toBe(false);
expect(findReleaseBlock().exists()).toBe(false);
});
});
...@@ -4,6 +4,7 @@ import { GlLink } from '@gitlab/ui'; ...@@ -4,6 +4,7 @@ import { GlLink } from '@gitlab/ui';
import ReleaseBlockHeader from '~/releases/components/release_block_header.vue'; import ReleaseBlockHeader from '~/releases/components/release_block_header.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { release as originalRelease } from '../mock_data'; import { release as originalRelease } from '../mock_data';
import { BACK_URL_PARAM } from '~/releases/constants';
describe('Release block header', () => { describe('Release block header', () => {
let wrapper; let wrapper;
...@@ -27,6 +28,7 @@ describe('Release block header', () => { ...@@ -27,6 +28,7 @@ describe('Release block header', () => {
const findHeader = () => wrapper.find('h2'); const findHeader = () => wrapper.find('h2');
const findHeaderLink = () => findHeader().find(GlLink); const findHeaderLink = () => findHeader().find(GlLink);
const findEditButton = () => wrapper.find('.js-edit-button');
describe('when _links.self is provided', () => { describe('when _links.self is provided', () => {
beforeEach(() => { beforeEach(() => {
...@@ -51,4 +53,39 @@ describe('Release block header', () => { ...@@ -51,4 +53,39 @@ describe('Release block header', () => {
expect(findHeaderLink().exists()).toBe(false); expect(findHeaderLink().exists()).toBe(false);
}); });
}); });
describe('when _links.edit_url is provided', () => {
const currentUrl = 'https://example.gitlab.com/path';
beforeEach(() => {
Object.defineProperty(window, 'location', {
writable: true,
value: {
href: currentUrl,
},
});
factory();
});
it('renders an edit button', () => {
expect(findEditButton().exists()).toBe(true);
});
it('renders the edit button with the correct href', () => {
const expectedQueryParam = `${BACK_URL_PARAM}=${encodeURIComponent(currentUrl)}`;
const expectedUrl = `${release._links.editUrl}?${expectedQueryParam}`;
expect(findEditButton().attributes().href).toBe(expectedUrl);
});
});
describe('when _links.edit is missing', () => {
beforeEach(() => {
factory({ _links: { editUrl: null } });
});
it('does not render an edit button', () => {
expect(findEditButton().exists()).toBe(false);
});
});
}); });
...@@ -7,20 +7,9 @@ import ReleaseBlockFooter from '~/releases/components/release_block_footer.vue'; ...@@ -7,20 +7,9 @@ import ReleaseBlockFooter from '~/releases/components/release_block_footer.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
import { release as originalRelease } from '../mock_data'; import { release as originalRelease } from '../mock_data';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { scrollToElement } from '~/lib/utils/common_utils'; import * as commonUtils from '~/lib/utils/common_utils';
import { BACK_URL_PARAM } from '~/releases/constants';
const { convertObjectPropsToCamelCase } = jest.requireActual('~/lib/utils/common_utils'); import * as urlUtility from '~/lib/utils/url_utility';
let mockLocationHash;
jest.mock('~/lib/utils/url_utility', () => ({
__esModule: true,
getLocationHash: jest.fn().mockImplementation(() => mockLocationHash),
}));
jest.mock('~/lib/utils/common_utils', () => ({
__esModule: true,
scrollToElement: jest.fn(),
}));
describe('Release block', () => { describe('Release block', () => {
let wrapper; let wrapper;
...@@ -47,7 +36,7 @@ describe('Release block', () => { ...@@ -47,7 +36,7 @@ describe('Release block', () => {
beforeEach(() => { beforeEach(() => {
jest.spyOn($.fn, 'renderGFM'); jest.spyOn($.fn, 'renderGFM');
release = convertObjectPropsToCamelCase(originalRelease, { deep: true }); release = commonUtils.convertObjectPropsToCamelCase(originalRelease, { deep: true });
}); });
afterEach(() => { afterEach(() => {
...@@ -61,9 +50,11 @@ describe('Release block', () => { ...@@ -61,9 +50,11 @@ describe('Release block', () => {
expect(wrapper.attributes().id).toBe('v0.3'); expect(wrapper.attributes().id).toBe('v0.3');
}); });
it('renders an edit button that links to the "Edit release" page', () => { it(`renders an edit button that links to the "Edit release" page with a "${BACK_URL_PARAM}" parameter`, () => {
expect(editButton().exists()).toBe(true); expect(editButton().exists()).toBe(true);
expect(editButton().attributes('href')).toBe(release._links.editUrl); expect(editButton().attributes('href')).toBe(
`${release._links.editUrl}?${BACK_URL_PARAM}=${encodeURIComponent(window.location.href)}`,
);
}); });
it('renders release name', () => { it('renders release name', () => {
...@@ -150,14 +141,6 @@ describe('Release block', () => { ...@@ -150,14 +141,6 @@ describe('Release block', () => {
}); });
}); });
it("does not render an edit button if release._links.editUrl isn't a string", () => {
delete release._links;
return factory(release).then(() => {
expect(editButton().exists()).toBe(false);
});
});
it('does not render the milestone list if no milestones are associated to the release', () => { it('does not render the milestone list if no milestones are associated to the release', () => {
delete release.milestones; delete release.milestones;
...@@ -203,37 +186,40 @@ describe('Release block', () => { ...@@ -203,37 +186,40 @@ describe('Release block', () => {
}); });
describe('anchor scrolling', () => { describe('anchor scrolling', () => {
let locationHash;
beforeEach(() => { beforeEach(() => {
scrollToElement.mockClear(); commonUtils.scrollToElement = jest.fn();
urlUtility.getLocationHash = jest.fn().mockImplementation(() => locationHash);
}); });
const hasTargetBlueBackground = () => wrapper.classes('bg-line-target-blue'); const hasTargetBlueBackground = () => wrapper.classes('bg-line-target-blue');
it('does not attempt to scroll the page if no anchor tag is included in the URL', () => { it('does not attempt to scroll the page if no anchor tag is included in the URL', () => {
mockLocationHash = ''; locationHash = '';
return factory(release).then(() => { return factory(release).then(() => {
expect(scrollToElement).not.toHaveBeenCalled(); expect(commonUtils.scrollToElement).not.toHaveBeenCalled();
}); });
}); });
it("does not attempt to scroll the page if the anchor tag doesn't match the release's tag name", () => { it("does not attempt to scroll the page if the anchor tag doesn't match the release's tag name", () => {
mockLocationHash = 'v0.4'; locationHash = 'v0.4';
return factory(release).then(() => { return factory(release).then(() => {
expect(scrollToElement).not.toHaveBeenCalled(); expect(commonUtils.scrollToElement).not.toHaveBeenCalled();
}); });
}); });
it("attempts to scroll itself into view if the anchor tag matches the release's tag name", () => { it("attempts to scroll itself into view if the anchor tag matches the release's tag name", () => {
mockLocationHash = release.tagName; locationHash = release.tagName;
return factory(release).then(() => { return factory(release).then(() => {
expect(scrollToElement).toHaveBeenCalledTimes(1); expect(commonUtils.scrollToElement).toHaveBeenCalledTimes(1);
expect(scrollToElement).toHaveBeenCalledWith(wrapper.element); expect(commonUtils.scrollToElement).toHaveBeenCalledWith(wrapper.element);
}); });
}); });
it('renders with a light blue background if it is the target of the anchor', () => { it('renders with a light blue background if it is the target of the anchor', () => {
mockLocationHash = release.tagName; locationHash = release.tagName;
return factory(release).then(() => { return factory(release).then(() => {
expect(hasTargetBlueBackground()).toBe(true); expect(hasTargetBlueBackground()).toBe(true);
...@@ -241,7 +227,7 @@ describe('Release block', () => { ...@@ -241,7 +227,7 @@ describe('Release block', () => {
}); });
it('does not render with a light blue background if it is not the target of the anchor', () => { it('does not render with a light blue background if it is not the target of the anchor', () => {
mockLocationHash = ''; locationHash = '';
return factory(release).then(() => { return factory(release).then(() => {
expect(hasTargetBlueBackground()).toBe(false); expect(hasTargetBlueBackground()).toBe(false);
......
import axios from 'axios'; import axios from 'axios';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper'; import testAction from 'helpers/vuex_action_helper';
import { cloneDeep, merge } from 'lodash';
import * as actions from '~/releases/stores/modules/detail/actions'; import * as actions from '~/releases/stores/modules/detail/actions';
import * as types from '~/releases/stores/modules/detail/mutation_types'; import * as types from '~/releases/stores/modules/detail/mutation_types';
import { release } from '../../../mock_data'; import { release as originalRelease } from '../../../mock_data';
import state from '~/releases/stores/modules/detail/state'; import createState from '~/releases/stores/modules/detail/state';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { redirectTo } from '~/lib/utils/url_utility';
jest.mock('~/flash', () => jest.fn()); jest.mock('~/flash', () => jest.fn());
...@@ -17,14 +18,14 @@ jest.mock('~/lib/utils/url_utility', () => ({ ...@@ -17,14 +18,14 @@ jest.mock('~/lib/utils/url_utility', () => ({
})); }));
describe('Release detail actions', () => { describe('Release detail actions', () => {
let stateClone; let state;
let releaseClone; let release;
let mock; let mock;
let error; let error;
beforeEach(() => { beforeEach(() => {
stateClone = state(); state = createState();
releaseClone = JSON.parse(JSON.stringify(release)); release = cloneDeep(originalRelease);
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
gon.api_version = 'v4'; gon.api_version = 'v4';
error = { message: 'An error occurred' }; error = { message: 'An error occurred' };
...@@ -39,7 +40,7 @@ describe('Release detail actions', () => { ...@@ -39,7 +40,7 @@ describe('Release detail actions', () => {
it(`commits ${types.SET_INITIAL_STATE} with the provided object`, () => { it(`commits ${types.SET_INITIAL_STATE} with the provided object`, () => {
const initialState = {}; const initialState = {};
return testAction(actions.setInitialState, initialState, stateClone, [ return testAction(actions.setInitialState, initialState, state, [
{ type: types.SET_INITIAL_STATE, payload: initialState }, { type: types.SET_INITIAL_STATE, payload: initialState },
]); ]);
}); });
...@@ -47,19 +48,19 @@ describe('Release detail actions', () => { ...@@ -47,19 +48,19 @@ describe('Release detail actions', () => {
describe('requestRelease', () => { describe('requestRelease', () => {
it(`commits ${types.REQUEST_RELEASE}`, () => it(`commits ${types.REQUEST_RELEASE}`, () =>
testAction(actions.requestRelease, undefined, stateClone, [{ type: types.REQUEST_RELEASE }])); testAction(actions.requestRelease, undefined, state, [{ type: types.REQUEST_RELEASE }]));
}); });
describe('receiveReleaseSuccess', () => { describe('receiveReleaseSuccess', () => {
it(`commits ${types.RECEIVE_RELEASE_SUCCESS}`, () => it(`commits ${types.RECEIVE_RELEASE_SUCCESS}`, () =>
testAction(actions.receiveReleaseSuccess, releaseClone, stateClone, [ testAction(actions.receiveReleaseSuccess, release, state, [
{ type: types.RECEIVE_RELEASE_SUCCESS, payload: releaseClone }, { type: types.RECEIVE_RELEASE_SUCCESS, payload: release },
])); ]));
}); });
describe('receiveReleaseError', () => { describe('receiveReleaseError', () => {
it(`commits ${types.RECEIVE_RELEASE_ERROR}`, () => it(`commits ${types.RECEIVE_RELEASE_ERROR}`, () =>
testAction(actions.receiveReleaseError, error, stateClone, [ testAction(actions.receiveReleaseError, error, state, [
{ type: types.RECEIVE_RELEASE_ERROR, payload: error }, { type: types.RECEIVE_RELEASE_ERROR, payload: error },
])); ]));
...@@ -77,24 +78,24 @@ describe('Release detail actions', () => { ...@@ -77,24 +78,24 @@ describe('Release detail actions', () => {
let getReleaseUrl; let getReleaseUrl;
beforeEach(() => { beforeEach(() => {
stateClone.projectId = '18'; state.projectId = '18';
stateClone.tagName = 'v1.3'; state.tagName = 'v1.3';
getReleaseUrl = `/api/v4/projects/${stateClone.projectId}/releases/${stateClone.tagName}`; getReleaseUrl = `/api/v4/projects/${state.projectId}/releases/${state.tagName}`;
}); });
it(`dispatches requestRelease and receiveReleaseSuccess with the camel-case'd release object`, () => { it(`dispatches requestRelease and receiveReleaseSuccess with the camel-case'd release object`, () => {
mock.onGet(getReleaseUrl).replyOnce(200, releaseClone); mock.onGet(getReleaseUrl).replyOnce(200, release);
return testAction( return testAction(
actions.fetchRelease, actions.fetchRelease,
undefined, undefined,
stateClone, state,
[], [],
[ [
{ type: 'requestRelease' }, { type: 'requestRelease' },
{ {
type: 'receiveReleaseSuccess', type: 'receiveReleaseSuccess',
payload: convertObjectPropsToCamelCase(releaseClone, { deep: true }), payload: convertObjectPropsToCamelCase(release, { deep: true }),
}, },
], ],
); );
...@@ -106,7 +107,7 @@ describe('Release detail actions', () => { ...@@ -106,7 +107,7 @@ describe('Release detail actions', () => {
return testAction( return testAction(
actions.fetchRelease, actions.fetchRelease,
undefined, undefined,
stateClone, state,
[], [],
[{ type: 'requestRelease' }, { type: 'receiveReleaseError', payload: expect.anything() }], [{ type: 'requestRelease' }, { type: 'receiveReleaseError', payload: expect.anything() }],
); );
...@@ -116,7 +117,7 @@ describe('Release detail actions', () => { ...@@ -116,7 +117,7 @@ describe('Release detail actions', () => {
describe('updateReleaseTitle', () => { describe('updateReleaseTitle', () => {
it(`commits ${types.UPDATE_RELEASE_TITLE} with the updated release title`, () => { it(`commits ${types.UPDATE_RELEASE_TITLE} with the updated release title`, () => {
const newTitle = 'The new release title'; const newTitle = 'The new release title';
return testAction(actions.updateReleaseTitle, newTitle, stateClone, [ return testAction(actions.updateReleaseTitle, newTitle, state, [
{ type: types.UPDATE_RELEASE_TITLE, payload: newTitle }, { type: types.UPDATE_RELEASE_TITLE, payload: newTitle },
]); ]);
}); });
...@@ -125,7 +126,7 @@ describe('Release detail actions', () => { ...@@ -125,7 +126,7 @@ describe('Release detail actions', () => {
describe('updateReleaseNotes', () => { describe('updateReleaseNotes', () => {
it(`commits ${types.UPDATE_RELEASE_NOTES} with the updated release notes`, () => { it(`commits ${types.UPDATE_RELEASE_NOTES} with the updated release notes`, () => {
const newReleaseNotes = 'The new release notes'; const newReleaseNotes = 'The new release notes';
return testAction(actions.updateReleaseNotes, newReleaseNotes, stateClone, [ return testAction(actions.updateReleaseNotes, newReleaseNotes, state, [
{ type: types.UPDATE_RELEASE_NOTES, payload: newReleaseNotes }, { type: types.UPDATE_RELEASE_NOTES, payload: newReleaseNotes },
]); ]);
}); });
...@@ -133,25 +134,40 @@ describe('Release detail actions', () => { ...@@ -133,25 +134,40 @@ describe('Release detail actions', () => {
describe('requestUpdateRelease', () => { describe('requestUpdateRelease', () => {
it(`commits ${types.REQUEST_UPDATE_RELEASE}`, () => it(`commits ${types.REQUEST_UPDATE_RELEASE}`, () =>
testAction(actions.requestUpdateRelease, undefined, stateClone, [ testAction(actions.requestUpdateRelease, undefined, state, [
{ type: types.REQUEST_UPDATE_RELEASE }, { type: types.REQUEST_UPDATE_RELEASE },
])); ]));
}); });
describe('receiveUpdateReleaseSuccess', () => { describe('receiveUpdateReleaseSuccess', () => {
it(`commits ${types.RECEIVE_UPDATE_RELEASE_SUCCESS}`, () => it(`commits ${types.RECEIVE_UPDATE_RELEASE_SUCCESS}`, () =>
testAction( testAction(actions.receiveUpdateReleaseSuccess, undefined, { ...state, featureFlags: {} }, [
actions.receiveUpdateReleaseSuccess, { type: types.RECEIVE_UPDATE_RELEASE_SUCCESS },
undefined, ]));
stateClone,
[{ type: types.RECEIVE_UPDATE_RELEASE_SUCCESS }], describe('when the releaseShowPage feature flag is enabled', () => {
[{ type: 'navigateToReleasesPage' }], const rootState = { featureFlags: { releaseShowPage: true } };
)); const updatedState = merge({}, state, {
releasesPagePath: 'path/to/releases/page',
release: {
_links: {
self: 'path/to/self',
},
},
});
actions.receiveUpdateReleaseSuccess({ commit: jest.fn(), state: updatedState, rootState });
expect(redirectTo).toHaveBeenCalledTimes(1);
expect(redirectTo).toHaveBeenCalledWith(updatedState.release._links.self);
});
describe('when the releaseShowPage feature flag is disabled', () => {});
}); });
describe('receiveUpdateReleaseError', () => { describe('receiveUpdateReleaseError', () => {
it(`commits ${types.RECEIVE_UPDATE_RELEASE_ERROR}`, () => it(`commits ${types.RECEIVE_UPDATE_RELEASE_ERROR}`, () =>
testAction(actions.receiveUpdateReleaseError, error, stateClone, [ testAction(actions.receiveUpdateReleaseError, error, state, [
{ type: types.RECEIVE_UPDATE_RELEASE_ERROR, payload: error }, { type: types.RECEIVE_UPDATE_RELEASE_ERROR, payload: error },
])); ]));
...@@ -169,10 +185,10 @@ describe('Release detail actions', () => { ...@@ -169,10 +185,10 @@ describe('Release detail actions', () => {
let getReleaseUrl; let getReleaseUrl;
beforeEach(() => { beforeEach(() => {
stateClone.release = releaseClone; state.release = release;
stateClone.projectId = '18'; state.projectId = '18';
stateClone.tagName = 'v1.3'; state.tagName = 'v1.3';
getReleaseUrl = `/api/v4/projects/${stateClone.projectId}/releases/${stateClone.tagName}`; getReleaseUrl = `/api/v4/projects/${state.projectId}/releases/${state.tagName}`;
}); });
it(`dispatches requestUpdateRelease and receiveUpdateReleaseSuccess`, () => { it(`dispatches requestUpdateRelease and receiveUpdateReleaseSuccess`, () => {
...@@ -181,7 +197,7 @@ describe('Release detail actions', () => { ...@@ -181,7 +197,7 @@ describe('Release detail actions', () => {
return testAction( return testAction(
actions.updateRelease, actions.updateRelease,
undefined, undefined,
stateClone, state,
[], [],
[{ type: 'requestUpdateRelease' }, { type: 'receiveUpdateReleaseSuccess' }], [{ type: 'requestUpdateRelease' }, { type: 'receiveUpdateReleaseSuccess' }],
); );
...@@ -193,7 +209,7 @@ describe('Release detail actions', () => { ...@@ -193,7 +209,7 @@ describe('Release detail actions', () => {
return testAction( return testAction(
actions.updateRelease, actions.updateRelease,
undefined, undefined,
stateClone, state,
[], [],
[ [
{ type: 'requestUpdateRelease' }, { type: 'requestUpdateRelease' },
...@@ -202,16 +218,4 @@ describe('Release detail actions', () => { ...@@ -202,16 +218,4 @@ describe('Release detail actions', () => {
); );
}); });
}); });
describe('navigateToReleasesPage', () => {
it(`calls redirectTo() with the URL to the releases page`, () => {
const releasesPagePath = 'path/to/releases/page';
stateClone.releasesPagePath = releasesPagePath;
actions.navigateToReleasesPage({ state: stateClone });
expect(redirectTo).toHaveBeenCalledTimes(1);
expect(redirectTo).toHaveBeenCalledWith(releasesPagePath);
});
});
}); });
...@@ -531,8 +531,10 @@ describe MarkupHelper do ...@@ -531,8 +531,10 @@ describe MarkupHelper do
it 'preserves style attribute for a label that can be accessed by current_user' do it 'preserves style attribute for a label that can be accessed by current_user' do
project = create(:project, :public) project = create(:project, :public)
label = create_and_format_label(project)
expect(create_and_format_label(project)).to match(/span class=.*style=.*/) expect(label).to match(/span class=.*style=.*/)
expect(label).to include('data-html="true"')
end end
it 'does not style a label that can not be accessed by current_user' do it 'does not style a label that can not be accessed by current_user' do
...@@ -544,6 +546,15 @@ describe MarkupHelper do ...@@ -544,6 +546,15 @@ describe MarkupHelper do
end end
end end
it 'keeps whitelisted tags' do
html = '<a><i></i></a> <strong>strong</strong><em>em</em><b>b</b>'
object = create_object(html)
result = first_line_in_markdown(object, attribute, 100, project: project)
expect(result).to include(html)
end
it 'truncates Markdown properly' do it 'truncates Markdown properly' do
object = create_object("@#{user.username}, can you look at this?\nHello world\n") object = create_object("@#{user.username}, can you look at this?\nHello world\n")
actual = first_line_in_markdown(object, attribute, 100, project: project) actual = first_line_in_markdown(object, attribute, 100, project: project)
......
...@@ -27,7 +27,7 @@ describe('Releases App ', () => { ...@@ -27,7 +27,7 @@ describe('Releases App ', () => {
}; };
beforeEach(() => { beforeEach(() => {
store = createStore({ list: listModule }); store = createStore({ modules: { list: listModule } });
releasesPagination = _.range(21).map(index => ({ releasesPagination = _.range(21).map(index => ({
...convertObjectPropsToCamelCase(release, { deep: true }), ...convertObjectPropsToCamelCase(release, { deep: true }),
tagName: `${index}.00`, tagName: `${index}.00`,
......
...@@ -425,16 +425,16 @@ describe Issue do ...@@ -425,16 +425,16 @@ describe Issue do
let(:issue) { create(:issue, title: 'testing-issue') } let(:issue) { create(:issue, title: 'testing-issue') }
it 'starts with the issue iid' do it 'starts with the issue iid' do
expect(issue.to_branch_name).to match /\A#{issue.iid}-[A-Za-z\-]+\z/ expect(issue.to_branch_name).to match(/\A#{issue.iid}-[A-Za-z\-]+\z/)
end end
it "contains the issue title if not confidential" do it "contains the issue title if not confidential" do
expect(issue.to_branch_name).to match /testing-issue\z/ expect(issue.to_branch_name).to match(/testing-issue\z/)
end end
it "does not contain the issue title if confidential" do it "does not contain the issue title if confidential" do
issue = create(:issue, title: 'testing-issue', confidential: true) issue = create(:issue, title: 'testing-issue', confidential: true)
expect(issue.to_branch_name).to match /confidential-issue\z/ expect(issue.to_branch_name).to match(/confidential-issue\z/)
end end
context 'issue title longer than 100 characters' do context 'issue title longer than 100 characters' do
...@@ -932,4 +932,33 @@ describe Issue do ...@@ -932,4 +932,33 @@ describe Issue do
end end
it_behaves_like 'versioned description' it_behaves_like 'versioned description'
describe "#previous_updated_at" do
let_it_be(:updated_at) { Time.new(2012, 01, 06) }
let_it_be(:issue) { create(:issue, updated_at: updated_at) }
it 'returns updated_at value if updated_at did not change at all' do
allow(issue).to receive(:previous_changes).and_return({})
expect(issue.previous_updated_at).to eq(updated_at)
end
it 'returns updated_at value if `previous_changes` has nil value for `updated_at`' do
allow(issue).to receive(:previous_changes).and_return({ 'updated_at' => nil })
expect(issue.previous_updated_at).to eq(updated_at)
end
it 'returns updated_at value if previous updated_at value is not present' do
allow(issue).to receive(:previous_changes).and_return({ 'updated_at' => [nil, Time.new(2013, 02, 06)] })
expect(issue.previous_updated_at).to eq(updated_at)
end
it 'returns previous updated_at when present' do
allow(issue).to receive(:previous_changes).and_return({ 'updated_at' => [Time.new(2013, 02, 06), Time.new(2013, 03, 06)] })
expect(issue.previous_updated_at).to eq(Time.new(2013, 02, 06))
end
end
end end
...@@ -67,7 +67,7 @@ RSpec.describe ResourceWeightEvent, type: :model do ...@@ -67,7 +67,7 @@ RSpec.describe ResourceWeightEvent, type: :model do
it 'returns the expected id' do it 'returns the expected id' do
allow(Digest::SHA1).to receive(:hexdigest) allow(Digest::SHA1).to receive(:hexdigest)
.with("ResourceWeightEvent-2019-12-30 00:00:00 UTC-#{user1.id}") .with("ResourceWeightEvent-#{event.id}-#{user1.id}")
.and_return('73d167c478') .and_return('73d167c478')
expect(event.discussion_id).to eq('73d167c478') expect(event.discussion_id).to eq('73d167c478')
......
...@@ -26,7 +26,7 @@ module FilteredSearchHelpers ...@@ -26,7 +26,7 @@ module FilteredSearchHelpers
# Select a label clicking in the search dropdown instead # Select a label clicking in the search dropdown instead
# of entering label names on the input. # of entering label names on the input.
def select_label_on_dropdown(label_title) def select_label_on_dropdown(label_title)
input_filtered_search("label=", submit: false) input_filtered_search("label:=", submit: false)
within('#js-dropdown-label') do within('#js-dropdown-label') do
wait_for_requests wait_for_requests
...@@ -71,7 +71,7 @@ module FilteredSearchHelpers ...@@ -71,7 +71,7 @@ module FilteredSearchHelpers
end end
def init_label_search def init_label_search
filtered_search.set('label=') filtered_search.set('label:=')
# This ensures the dropdown is shown # This ensures the dropdown is shown
expect(find('#js-dropdown-label')).not_to have_css('.filter-dropdown-loading') expect(find('#js-dropdown-label')).not_to have_css('.filter-dropdown-loading')
end end
......
...@@ -13,7 +13,7 @@ RSpec.shared_examples 'issuable user dropdown behaviors' do ...@@ -13,7 +13,7 @@ RSpec.shared_examples 'issuable user dropdown behaviors' do
it 'only includes members of the project/group' do it 'only includes members of the project/group' do
visit issuables_path visit issuables_path
filtered_search.set("#{dropdown}=") filtered_search.set("#{dropdown}:=")
expect(find("#js-dropdown-#{dropdown} .filter-dropdown")).to have_content(user_in_dropdown.name) expect(find("#js-dropdown-#{dropdown} .filter-dropdown")).to have_content(user_in_dropdown.name)
expect(find("#js-dropdown-#{dropdown} .filter-dropdown")).not_to have_content(user_not_in_dropdown.name) expect(find("#js-dropdown-#{dropdown} .filter-dropdown")).not_to have_content(user_not_in_dropdown.name)
......
...@@ -11453,6 +11453,11 @@ underscore@~1.8.3: ...@@ -11453,6 +11453,11 @@ underscore@~1.8.3:
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022"
integrity sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI= integrity sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=
unfetch@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.1.0.tgz#6ec2dd0de887e58a4dee83a050ded80ffc4137db"
integrity sha512-crP/n3eAPUJxZXM9T80/yv0YhkTEx2K1D3h7D1AJM6fzsWZrxdyRuLN0JH/dkZh1LNH8LxCnBzoPFCPbb2iGpg==
unherit@^1.0.4: unherit@^1.0.4:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/unherit/-/unherit-1.1.1.tgz#132748da3e88eab767e08fabfbb89c5e9d28628c" resolved "https://registry.yarnpkg.com/unherit/-/unherit-1.1.1.tgz#132748da3e88eab767e08fabfbb89c5e9d28628c"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment