Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-03-04 12:08:20 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-03-04 12:08:20 +0300
commitd80f3cd75e700b6e62910865bfd36734644ffa89 (patch)
treeaa2fa2f2b4385854c13591bef8e74924ef661657
parentbe81c1578d65f25edfde8aa550f190b8d3e6d976 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue85
-rw-r--r--app/assets/javascripts/blob/suggest_gitlab_ci_yml/index.js16
-rw-r--r--app/assets/javascripts/commons/polyfills.js19
-rw-r--r--app/assets/javascripts/commons/polyfills/custom_event.js7
-rw-r--r--app/assets/javascripts/commons/polyfills/element.js32
-rw-r--r--app/assets/javascripts/commons/polyfills/event.js8
-rw-r--r--app/assets/javascripts/commons/polyfills/nodelist.js7
-rw-r--r--app/assets/javascripts/commons/polyfills/request_idle_callback.js7
-rw-r--r--app/assets/javascripts/commons/polyfills/svg.js10
-rw-r--r--app/assets/javascripts/emoji/index.js4
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js27
-rw-r--r--app/assets/javascripts/monitoring/components/charts/column.vue22
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js3
-rw-r--r--app/assets/javascripts/pages/projects/blob/new/index.js11
-rw-r--r--app/assets/javascripts/pages/projects/releases/show/index.js3
-rw-r--r--app/assets/javascripts/releases/components/app_edit.vue19
-rw-r--r--app/assets/javascripts/releases/components/app_show.vue29
-rw-r--r--app/assets/javascripts/releases/components/release_block_header.vue12
-rw-r--r--app/assets/javascripts/releases/constants.js8
-rw-r--r--app/assets/javascripts/releases/mount_edit.js10
-rw-r--r--app/assets/javascripts/releases/mount_index.js6
-rw-r--r--app/assets/javascripts/releases/mount_show.js21
-rw-r--r--app/assets/javascripts/releases/stores/index.js6
-rw-r--r--app/assets/javascripts/releases/stores/modules/detail/actions.js6
-rw-r--r--app/assets/stylesheets/components/popover.scss12
-rw-r--r--app/assets/stylesheets/framework/variables.scss1
-rw-r--r--app/assets/stylesheets/notify.scss16
-rw-r--r--app/assets/stylesheets/pages/projects.scss8
-rw-r--r--app/helpers/markup_helper.rb6
-rw-r--r--app/helpers/suggest_pipeline_helper.rb9
-rw-r--r--app/models/issue.rb4
-rw-r--r--app/models/milestone_note.rb1
-rw-r--r--app/models/resource_event.rb2
-rw-r--r--app/models/resource_label_event.rb4
-rw-r--r--app/views/dashboard/projects/_projects.html.haml2
-rw-r--r--app/views/explore/projects/_projects.html.haml2
-rw-r--r--app/views/projects/blob/_editor.html.haml4
-rw-r--r--app/views/projects/blob/_suggest_gitlab_ci_yml.html.haml4
-rw-r--r--app/views/projects/blob/_template_selectors.html.haml5
-rw-r--r--app/views/projects/runners/_runner.html.haml2
-rw-r--r--app/views/shared/projects/_project.html.haml7
-rw-r--r--changelogs/unreleased/195871-fix-duplicate-weight-change-notes.yml5
-rw-r--r--changelogs/unreleased/201999-formatter-column-chart.yml5
-rw-r--r--changelogs/unreleased/207462-scoped-labels-rendering-is-broken-in-todos.yml5
-rw-r--r--changelogs/unreleased/208242-scoped-label-rendering-in-emails-is-broken.yml5
-rw-r--r--changelogs/unreleased/208471-actionview-template-error-undefined-method-concat-for-nil-nilclass.yml5
-rw-r--r--changelogs/unreleased/208524-error-in-custom-dashboard-yml-file-breaks-the-dashboards-dropdown.yml5
-rw-r--r--changelogs/unreleased/63-nudge-users-to-select-a-template-to-set-up-a-pipeline.yml5
-rw-r--r--changelogs/unreleased/jswain_update_renewal_link.yml5
-rw-r--r--changelogs/unreleased/leipert-polyfills-improvements.yml5
-rw-r--r--changelogs/unreleased/revert-e0613e64.yml5
-rw-r--r--changelogs/unreleased/tokenize-filtered-search.yml5
-rw-r--r--locale/gitlab.pot12
-rw-r--r--package.json1
-rw-r--r--spec/features/admin/admin_runners_spec.rb24
-rw-r--r--spec/features/boards/boards_spec.rb2
-rw-r--r--spec/features/boards/modal_filter_spec.rb2
-rw-r--r--spec/features/dashboard/issues_filter_spec.rb6
-rw-r--r--spec/features/dashboard/issues_spec.rb2
-rw-r--r--spec/features/dashboard/merge_requests_spec.rb4
-rw-r--r--spec/features/dashboard/projects_spec.rb55
-rw-r--r--spec/features/groups/issues_spec.rb2
-rw-r--r--spec/features/groups/merge_requests_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/dropdown_assignee_spec.rb6
-rw-r--r--spec/features/issues/filtered_search/dropdown_author_spec.rb6
-rw-r--r--spec/features/issues/filtered_search/dropdown_base_spec.rb8
-rw-r--r--spec/features/issues/filtered_search/dropdown_emoji_spec.rb6
-rw-r--r--spec/features/issues/filtered_search/dropdown_label_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/dropdown_milestone_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/dropdown_release_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb72
-rw-r--r--spec/features/issues/filtered_search/visual_tokens_spec.rb16
-rw-r--r--spec/features/merge_requests/filters_generic_behavior_spec.rb2
-rw-r--r--spec/features/merge_requests/user_filters_by_assignees_spec.rb4
-rw-r--r--spec/features/merge_requests/user_filters_by_labels_spec.rb6
-rw-r--r--spec/features/merge_requests/user_filters_by_milestones_spec.rb10
-rw-r--r--spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb4
-rw-r--r--spec/features/merge_requests/user_filters_by_target_branch_spec.rb6
-rw-r--r--spec/features/projects/blobs/user_follows_pipeline_suggest_nudge_spec.rb67
-rw-r--r--spec/features/projects/releases/user_views_edit_release_spec.rb58
-rw-r--r--spec/features/projects/releases/user_views_release_spec.rb37
-rw-r--r--spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js46
-rw-r--r--spec/frontend/monitoring/components/charts/column_spec.js59
-rw-r--r--spec/frontend/monitoring/mock_data.js6
-rw-r--r--spec/frontend/monitoring/store/actions_spec.js20
-rw-r--r--spec/frontend/releases/components/app_edit_spec.js136
-rw-r--r--spec/frontend/releases/components/app_show_spec.js61
-rw-r--r--spec/frontend/releases/components/release_block_header_spec.js37
-rw-r--r--spec/frontend/releases/components/release_block_spec.js56
-rw-r--r--spec/frontend/releases/stores/modules/detail/actions_spec.js100
-rw-r--r--spec/helpers/markup_helper_spec.rb13
-rw-r--r--spec/javascripts/releases/components/app_index_spec.js2
-rw-r--r--spec/models/issue_spec.rb35
-rw-r--r--spec/models/resource_weight_event_spec.rb2
-rw-r--r--spec/support/helpers/filtered_search_helpers.rb4
-rw-r--r--spec/support/shared_examples/features/issuables_user_dropdown_behaviors_shared_examples.rb2
-rw-r--r--yarn.lock5
97 files changed, 1180 insertions, 352 deletions
diff --git a/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue b/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue
new file mode 100644
index 00000000000..fa3c19921df
--- /dev/null
+++ b/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue
@@ -0,0 +1,85 @@
+<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>
diff --git a/app/assets/javascripts/blob/suggest_gitlab_ci_yml/index.js b/app/assets/javascripts/blob/suggest_gitlab_ci_yml/index.js
new file mode 100644
index 00000000000..f770000eb68
--- /dev/null
+++ b/app/assets/javascripts/blob/suggest_gitlab_ci_yml/index.js
@@ -0,0 +1,16 @@
+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,
+ },
+ });
+ },
+ });
diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js
index 5e04b0573d2..fdeb64a7644 100644
--- a/app/assets/javascripts/commons/polyfills.js
+++ b/app/assets/javascripts/commons/polyfills.js
@@ -1,5 +1,24 @@
// 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 './polyfills/custom_event';
import './polyfills/element';
import './polyfills/event';
diff --git a/app/assets/javascripts/commons/polyfills/custom_event.js b/app/assets/javascripts/commons/polyfills/custom_event.js
index db51ade61ae..6b14eff6f05 100644
--- a/app/assets/javascripts/commons/polyfills/custom_event.js
+++ b/app/assets/javascripts/commons/polyfills/custom_event.js
@@ -1,3 +1,10 @@
+/**
+ * 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') {
window.CustomEvent = function CustomEvent(event, params) {
const evt = document.createEvent('CustomEvent');
diff --git a/app/assets/javascripts/commons/polyfills/element.js b/app/assets/javascripts/commons/polyfills/element.js
index dde5e8f54f9..b13ceccf511 100644
--- a/app/assets/javascripts/commons/polyfills/element.js
+++ b/app/assets/javascripts/commons/polyfills/element.js
@@ -1,6 +1,19 @@
-// 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';
+/**
+ * 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 ||
function closest(selector, selectedElement = this) {
@@ -10,6 +23,13 @@ Element.prototype.closest =
: 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.matchesSelector ||
@@ -26,7 +46,15 @@ Element.prototype.matches =
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.forEach(item => {
if (Object.prototype.hasOwnProperty.call(item, 'remove')) {
diff --git a/app/assets/javascripts/commons/polyfills/event.js b/app/assets/javascripts/commons/polyfills/event.js
index ff5b9a1982f..543dd5f9a93 100644
--- a/app/assets/javascripts/commons/polyfills/event.js
+++ b/app/assets/javascripts/commons/polyfills/event.js
@@ -1,6 +1,10 @@
/**
- * Polyfill for IE11 support.
- * new Event() is not supported by IE11.
+ * Polyfill: Event constructor
+ * @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
*/
if (typeof window.Event !== 'function') {
diff --git a/app/assets/javascripts/commons/polyfills/nodelist.js b/app/assets/javascripts/commons/polyfills/nodelist.js
index 3772c94b900..3a9111e64f8 100644
--- a/app/assets/javascripts/commons/polyfills/nodelist.js
+++ b/app/assets/javascripts/commons/polyfills/nodelist.js
@@ -1,3 +1,10 @@
+/**
+ * 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) {
NodeList.prototype.forEach = function forEach(callback, thisArg = window) {
for (let i = 0; i < this.length; i += 1) {
diff --git a/app/assets/javascripts/commons/polyfills/request_idle_callback.js b/app/assets/javascripts/commons/polyfills/request_idle_callback.js
index 2356569d06e..51dc82e593a 100644
--- a/app/assets/javascripts/commons/polyfills/request_idle_callback.js
+++ b/app/assets/javascripts/commons/polyfills/request_idle_callback.js
@@ -1,3 +1,10 @@
+/**
+ * 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 ||
function requestShim(cb) {
diff --git a/app/assets/javascripts/commons/polyfills/svg.js b/app/assets/javascripts/commons/polyfills/svg.js
index 8648a568f6f..92a8b03fbb4 100644
--- a/app/assets/javascripts/commons/polyfills/svg.js
+++ b/app/assets/javascripts/commons/polyfills/svg.js
@@ -1,5 +1,11 @@
+/**
+ * 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';
-// polyfill support for external SVG file references via <use xlink:href>
-// @see https://css-tricks.com/svg-use-external-source/
svg4everybody();
diff --git a/app/assets/javascripts/emoji/index.js b/app/assets/javascripts/emoji/index.js
index cd8dff40b88..27dff8cf9aa 100644
--- a/app/assets/javascripts/emoji/index.js
+++ b/app/assets/javascripts/emoji/index.js
@@ -1,4 +1,4 @@
-import _ from 'underscore';
+import { uniq } from 'lodash';
import emojiMap from 'emojis/digests.json';
import emojiAliases from 'emojis/aliases.json';
@@ -18,7 +18,7 @@ export function filterEmojiNames(filter) {
}
export function filterEmojiNamesByAlias(filter) {
- return _.uniq(filterEmojiNames(filter).map(name => normalizeEmojiName(name)));
+ return uniq(filterEmojiNames(filter).map(name => normalizeEmojiName(name)));
}
let emojiCategoryMap;
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index e9a714605c7..88737396113 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -472,33 +472,6 @@ export default class FilteredSearchManager {
});
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()) {
const tokenKey = FilteredSearchVisualTokens.getLastTokenPartial();
const tokenOperator = searchToken && searchToken.trim();
diff --git a/app/assets/javascripts/monitoring/components/charts/column.vue b/app/assets/javascripts/monitoring/components/charts/column.vue
index 82857424ff7..0ed801e6e57 100644
--- a/app/assets/javascripts/monitoring/components/charts/column.vue
+++ b/app/assets/javascripts/monitoring/components/charts/column.vue
@@ -5,6 +5,7 @@ import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
import { chartHeight } from '../../constants';
import { makeDataSeries } from '~/helpers/monitor_helper';
import { graphDataValidatorForValues } from '../../utils';
+import { getYAxisOptions, getChartGrid } from './options';
export default {
components: {
@@ -41,15 +42,25 @@ export default {
values: queryData[0].data,
};
},
+ chartOptions() {
+ const yAxis = {
+ ...getYAxisOptions(this.graphData.yAxis),
+ scale: false,
+ };
+
+ return {
+ grid: getChartGrid(),
+ yAxis,
+ dataZoom: this.dataZoomConfig,
+ };
+ },
xAxisTitle() {
return this.graphData.metrics[0].result[0].x_label !== undefined
? this.graphData.metrics[0].result[0].x_label
: '';
},
yAxisTitle() {
- return this.graphData.metrics[0].result[0].y_label !== undefined
- ? this.graphData.metrics[0].result[0].y_label
- : '';
+ return this.chartOptions.yAxis.name;
},
xAxisType() {
return this.graphData.x_type !== undefined ? this.graphData.x_type : 'category';
@@ -59,11 +70,6 @@ export default {
return handleIcon ? { handleIcon } : {};
},
- chartOptions() {
- return {
- dataZoom: this.dataZoomConfig,
- };
- },
},
created() {
this.setSvg('scroll-handle');
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index aa6c35d97be..7d0d37c1a20 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -79,7 +79,7 @@ export const fetchData = ({ dispatch }) => {
dispatch('fetchEnvironmentsData');
};
-export const fetchDashboard = ({ state, dispatch }) => {
+export const fetchDashboard = ({ state, commit, dispatch }) => {
dispatch('requestMetricsDashboard');
const params = {};
@@ -100,6 +100,7 @@ export const fetchDashboard = ({ state, dispatch }) => {
.catch(error => {
Sentry.captureException(error);
+ commit(types.SET_ALL_DASHBOARDS, error.response?.data?.all_dashboards ?? []);
dispatch('receiveMetricsDashboardFailure', error);
if (state.showErrorBanner) {
diff --git a/app/assets/javascripts/pages/projects/blob/new/index.js b/app/assets/javascripts/pages/projects/blob/new/index.js
index 189053f3ed7..720cb249052 100644
--- a/app/assets/javascripts/pages/projects/blob/new/index.js
+++ b/app/assets/javascripts/pages/projects/blob/new/index.js
@@ -1,3 +1,12 @@
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);
+ }
+});
diff --git a/app/assets/javascripts/pages/projects/releases/show/index.js b/app/assets/javascripts/pages/projects/releases/show/index.js
new file mode 100644
index 00000000000..4e17e6ff311
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/releases/show/index.js
@@ -0,0 +1,3 @@
+import initShowRelease from '~/releases/mount_show';
+
+document.addEventListener('DOMContentLoaded', initShowRelease);
diff --git a/app/assets/javascripts/releases/components/app_edit.vue b/app/assets/javascripts/releases/components/app_edit.vue
index f6a4d00692e..6f4baaa5d74 100644
--- a/app/assets/javascripts/releases/components/app_edit.vue
+++ b/app/assets/javascripts/releases/components/app_edit.vue
@@ -1,10 +1,12 @@
<script>
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 { __, sprintf } from '~/locale';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
+import { BACK_URL_PARAM } from '~/releases/constants';
+import { getParameterByName } from '~/lib/utils/common_utils';
export default {
name: 'ReleaseEditApp',
@@ -12,6 +14,7 @@ export default {
GlFormInput,
GlFormGroup,
GlButton,
+ GlLink,
MarkdownField,
},
directives: {
@@ -74,6 +77,9 @@ export default {
this.updateReleaseNotes(notes);
},
},
+ cancelPath() {
+ return getParameterByName(BACK_URL_PARAM) || this.releasesPagePath;
+ },
},
created() {
this.fetchRelease();
@@ -84,7 +90,6 @@ export default {
'updateRelease',
'updateReleaseTitle',
'updateReleaseNotes',
- 'navigateToReleasesPage',
]),
},
};
@@ -157,15 +162,9 @@ export default {
>
{{ __('Save changes') }}
</gl-button>
- <gl-button
- class="js-cancel-button"
- variant="default"
- type="button"
- :aria-label="__('Cancel')"
- @click="navigateToReleasesPage()"
- >
+ <gl-link :href="cancelPath" class="js-cancel-button btn btn-default">
{{ __('Cancel') }}
- </gl-button>
+ </gl-link>
</div>
</form>
</div>
diff --git a/app/assets/javascripts/releases/components/app_show.vue b/app/assets/javascripts/releases/components/app_show.vue
new file mode 100644
index 00000000000..d521edcc361
--- /dev/null
+++ b/app/assets/javascripts/releases/components/app_show.vue
@@ -0,0 +1,29 @@
+<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>
diff --git a/app/assets/javascripts/releases/components/release_block_header.vue b/app/assets/javascripts/releases/components/release_block_header.vue
index 0bc2a5ce2eb..6f7e1dcfe2f 100644
--- a/app/assets/javascripts/releases/components/release_block_header.vue
+++ b/app/assets/javascripts/releases/components/release_block_header.vue
@@ -1,6 +1,8 @@
<script>
import { GlTooltipDirective, GlLink, GlBadge } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
+import { BACK_URL_PARAM } from '~/releases/constants';
+import { setUrlParams } from '~/lib/utils/url_utility';
export default {
name: 'ReleaseBlockHeader',
@@ -20,7 +22,15 @@ export default {
},
computed: {
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() {
return this.release._links?.self;
diff --git a/app/assets/javascripts/releases/constants.js b/app/assets/javascripts/releases/constants.js
index defcd917465..1db93323a87 100644
--- a/app/assets/javascripts/releases/constants.js
+++ b/app/assets/javascripts/releases/constants.js
@@ -1,7 +1,3 @@
-/* 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 BACK_URL_PARAM = 'back_url';
diff --git a/app/assets/javascripts/releases/mount_edit.js b/app/assets/javascripts/releases/mount_edit.js
index 2bc2728312a..102c4367aac 100644
--- a/app/assets/javascripts/releases/mount_edit.js
+++ b/app/assets/javascripts/releases/mount_edit.js
@@ -6,7 +6,15 @@ import detailModule from './stores/modules/detail';
export default () => {
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);
return new Vue({
diff --git a/app/assets/javascripts/releases/mount_index.js b/app/assets/javascripts/releases/mount_index.js
index 6fcb6d802e4..ad82d9a65d6 100644
--- a/app/assets/javascripts/releases/mount_index.js
+++ b/app/assets/javascripts/releases/mount_index.js
@@ -8,7 +8,11 @@ export default () => {
return new Vue({
el,
- store: createStore({ list: listModule }),
+ store: createStore({
+ modules: {
+ list: listModule,
+ },
+ }),
render: h =>
h(ReleaseListApp, {
props: {
diff --git a/app/assets/javascripts/releases/mount_show.js b/app/assets/javascripts/releases/mount_show.js
new file mode 100644
index 00000000000..73e34869b21
--- /dev/null
+++ b/app/assets/javascripts/releases/mount_show.js
@@ -0,0 +1,21 @@
+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),
+ });
+};
diff --git a/app/assets/javascripts/releases/stores/index.js b/app/assets/javascripts/releases/stores/index.js
index aa607906a0e..7f211145ccf 100644
--- a/app/assets/javascripts/releases/stores/index.js
+++ b/app/assets/javascripts/releases/stores/index.js
@@ -3,4 +3,8 @@ import Vuex from 'vuex';
Vue.use(Vuex);
-export default modules => new Vuex.Store({ modules });
+export default ({ modules, featureFlags }) =>
+ new Vuex.Store({
+ modules,
+ state: { featureFlags },
+ });
diff --git a/app/assets/javascripts/releases/stores/modules/detail/actions.js b/app/assets/javascripts/releases/stores/modules/detail/actions.js
index f730af1c7dc..35901a654b0 100644
--- a/app/assets/javascripts/releases/stores/modules/detail/actions.js
+++ b/app/assets/javascripts/releases/stores/modules/detail/actions.js
@@ -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 requestUpdateRelease = ({ commit }) => commit(types.REQUEST_UPDATE_RELEASE);
-export const receiveUpdateReleaseSuccess = ({ commit, dispatch }) => {
+export const receiveUpdateReleaseSuccess = ({ commit, state, rootState }) => {
commit(types.RECEIVE_UPDATE_RELEASE_SUCCESS);
- dispatch('navigateToReleasesPage');
+ redirectTo(
+ rootState.featureFlags.releaseShowPage ? state.release._links.self : state.releasesPagePath,
+ );
};
export const receiveUpdateReleaseError = ({ commit }, error) => {
commit(types.RECEIVE_UPDATE_RELEASE_ERROR, error);
diff --git a/app/assets/stylesheets/components/popover.scss b/app/assets/stylesheets/components/popover.scss
index 6654553aaa2..7e824c72f77 100644
--- a/app/assets/stylesheets/components/popover.scss
+++ b/app/assets/stylesheets/components/popover.scss
@@ -138,3 +138,15 @@
max-width: 40%;
}
}
+
+.suggest-gitlab-ci-yml {
+ margin-top: -1em;
+
+ .popover-header {
+ padding: $gl-padding;
+
+ .ic-close {
+ margin-top: -1em;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index e4853ca7bf5..d31d9245e9c 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -81,6 +81,7 @@ $gl-gray-400: #999;
$gl-gray-500: #777;
$gl-gray-600: #666;
$gl-gray-700: #555;
+$gl-gray-800: #333;
$green-50: #f1fdf6;
$green-100: #dcf5e7;
diff --git a/app/assets/stylesheets/notify.scss b/app/assets/stylesheets/notify.scss
index d77b7dfad68..ea82ba3e879 100644
--- a/app/assets/stylesheets/notify.scss
+++ b/app/assets/stylesheets/notify.scss
@@ -18,3 +18,19 @@ p.details {
pre.commit-message {
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;
+}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index f8832047d49..8b2c67378d9 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -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 {
display: inline-flex;
}
diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb
index a0228c6bd94..4f66356c27e 100644
--- a/app/helpers/markup_helper.rb
+++ b/app/helpers/markup_helper.rb
@@ -79,7 +79,7 @@ module MarkupHelper
md = markdown_field(object, attribute, options.merge(post_process: false))
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]
text = truncate_visible(md, max_chars || md.length)
@@ -88,7 +88,7 @@ module MarkupHelper
text,
tags: tags,
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
@@ -233,7 +233,7 @@ module MarkupHelper
def strip_empty_link_tags(text)
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
sanitize text, scrubber: scrubber
diff --git a/app/helpers/suggest_pipeline_helper.rb b/app/helpers/suggest_pipeline_helper.rb
new file mode 100644
index 00000000000..aa67f0ea770
--- /dev/null
+++ b/app/helpers/suggest_pipeline_helper.rb
@@ -0,0 +1,9 @@
+# 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
diff --git a/app/models/issue.rb b/app/models/issue.rb
index f265b72f11f..d3f597c0bda 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -305,6 +305,10 @@ class Issue < ApplicationRecord
labels.map(&:hook_attrs)
end
+ def previous_updated_at
+ previous_changes['updated_at']&.first || updated_at
+ end
+
private
def ensure_metrics
diff --git a/app/models/milestone_note.rb b/app/models/milestone_note.rb
index 8ff0503502f..4b027b0782c 100644
--- a/app/models/milestone_note.rb
+++ b/app/models/milestone_note.rb
@@ -12,6 +12,7 @@ class MilestoneNote < ::Note
created_at: event.created_at,
noteable: resource,
milestone: event.milestone,
+ discussion_id: event.discussion_id,
event: event,
system_note_metadata: ::SystemNoteMetadata.new(action: 'milestone'),
resource_parent: resource_parent
diff --git a/app/models/resource_event.rb b/app/models/resource_event.rb
index 2c0052b0be3..86e11c2d568 100644
--- a/app/models/resource_event.rb
+++ b/app/models/resource_event.rb
@@ -21,7 +21,7 @@ class ResourceEvent < ApplicationRecord
private
def discussion_id_key
- [self.class.name, created_at, user_id]
+ [self.class.name, id, user_id]
end
def exactly_one_issuable
diff --git a/app/models/resource_label_event.rb b/app/models/resource_label_event.rb
index 970d4e1e562..8e66310f0c5 100644
--- a/app/models/resource_label_event.rb
+++ b/app/models/resource_label_event.rb
@@ -103,6 +103,10 @@ class ResourceLabelEvent < ResourceEvent
def resource_parent
issuable.project || issuable.group
end
+
+ def discussion_id_key
+ [self.class.name, created_at, user_id]
+ end
end
ResourceLabelEvent.prepend_if_ee('EE::ResourceLabelEvent')
diff --git a/app/views/dashboard/projects/_projects.html.haml b/app/views/dashboard/projects/_projects.html.haml
index ca201e626b8..5122164dbcb 100644
--- a/app/views/dashboard/projects/_projects.html.haml
+++ b/app/views/dashboard/projects/_projects.html.haml
@@ -1 +1 @@
-= 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
diff --git a/app/views/explore/projects/_projects.html.haml b/app/views/explore/projects/_projects.html.haml
index 35b32662b8a..d819c4ea554 100644
--- a/app/views/explore/projects/_projects.html.haml
+++ b/app/views/explore/projects/_projects.html.haml
@@ -1,2 +1,2 @@
- 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)
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index 961b873b571..738bca111cd 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -17,8 +17,10 @@
%span.pull-left.append-right-10
\/
= 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'
+ - 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
- if is_markdown
diff --git a/app/views/projects/blob/_suggest_gitlab_ci_yml.html.haml b/app/views/projects/blob/_suggest_gitlab_ci_yml.html.haml
new file mode 100644
index 00000000000..6b368033c1e
--- /dev/null
+++ b/app/views/projects/blob/_suggest_gitlab_ci_yml.html.haml
@@ -0,0 +1,4 @@
+.js-suggest-gitlab-ci-yml{ data: { toggle: 'popover',
+ target: target,
+ css_class: 'suggest-gitlab-ci-yml ml-4',
+ dismiss_key: dismiss_key } }
diff --git a/app/views/projects/blob/_template_selectors.html.haml b/app/views/projects/blob/_template_selectors.html.haml
index 5ecfa135446..2be95bc5541 100644
--- a/app/views/projects/blob/_template_selectors.html.haml
+++ b/app/views/projects/blob/_template_selectors.html.haml
@@ -1,12 +1,13 @@
.template-selectors-menu.gl-pl-2
.template-selector-dropdowns-wrap
.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
= 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
= 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) } } )
.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) } } )
diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml
index 55c702b967f..92680a70da2 100644
--- a/app/views/projects/runners/_runner.html.haml
+++ b/app/views/projects/runners/_runner.html.haml
@@ -3,7 +3,7 @@
= runner_status_icon(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?
= icon('lock', class: 'has-tooltip', title: _('Locked to current projects'))
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 144bb04e2a8..d29ba3eedc6 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -12,7 +12,9 @@
- css_class += " no-description" if project.description.blank? && !show_last_commit_as_description
- cache_key = project_list_cache_key(project, pipeline_status: pipeline_status)
- 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 << "with-pipeline-status" if show_pipeline_status_icon
- avatar_container_class = project.creator && use_creator_avatar ? '' : 'rect-avatar'
- license_name = project_license_name(project)
@@ -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(" ") }
.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
- if stars
= link_to project_starrers_path(project),
diff --git a/changelogs/unreleased/195871-fix-duplicate-weight-change-notes.yml b/changelogs/unreleased/195871-fix-duplicate-weight-change-notes.yml
new file mode 100644
index 00000000000..24bcfd97392
--- /dev/null
+++ b/changelogs/unreleased/195871-fix-duplicate-weight-change-notes.yml
@@ -0,0 +1,5 @@
+---
+title: Ensure weight changes no longer render duplicate system notes
+merge_request: 26014
+author:
+type: fixed
diff --git a/changelogs/unreleased/201999-formatter-column-chart.yml b/changelogs/unreleased/201999-formatter-column-chart.yml
new file mode 100644
index 00000000000..c5d8935f4f7
--- /dev/null
+++ b/changelogs/unreleased/201999-formatter-column-chart.yml
@@ -0,0 +1,5 @@
+---
+title: Use y-axis format configuration in column charts
+merge_request: 26356
+author:
+type: changed
diff --git a/changelogs/unreleased/207462-scoped-labels-rendering-is-broken-in-todos.yml b/changelogs/unreleased/207462-scoped-labels-rendering-is-broken-in-todos.yml
new file mode 100644
index 00000000000..d365e4cff58
--- /dev/null
+++ b/changelogs/unreleased/207462-scoped-labels-rendering-is-broken-in-todos.yml
@@ -0,0 +1,5 @@
+---
+title: Fix scoped labels rendering in To-Do List
+merge_request: 26146
+author:
+type: fixed
diff --git a/changelogs/unreleased/208242-scoped-label-rendering-in-emails-is-broken.yml b/changelogs/unreleased/208242-scoped-label-rendering-in-emails-is-broken.yml
new file mode 100644
index 00000000000..a2bce06c890
--- /dev/null
+++ b/changelogs/unreleased/208242-scoped-label-rendering-in-emails-is-broken.yml
@@ -0,0 +1,5 @@
+---
+title: Fix scoped labels rendering in emails
+merge_request: 26347
+author:
+type: fixed
diff --git a/changelogs/unreleased/208471-actionview-template-error-undefined-method-concat-for-nil-nilclass.yml b/changelogs/unreleased/208471-actionview-template-error-undefined-method-concat-for-nil-nilclass.yml
new file mode 100644
index 00000000000..3a4eb8596d8
--- /dev/null
+++ b/changelogs/unreleased/208471-actionview-template-error-undefined-method-concat-for-nil-nilclass.yml
@@ -0,0 +1,5 @@
+---
+title: Fix an error with concat method
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/208524-error-in-custom-dashboard-yml-file-breaks-the-dashboards-dropdown.yml b/changelogs/unreleased/208524-error-in-custom-dashboard-yml-file-breaks-the-dashboards-dropdown.yml
new file mode 100644
index 00000000000..971765fc88f
--- /dev/null
+++ b/changelogs/unreleased/208524-error-in-custom-dashboard-yml-file-breaks-the-dashboards-dropdown.yml
@@ -0,0 +1,5 @@
+---
+title: Fix dashboards dropdown if custom dashboard is broken
+merge_request: 26228
+author:
+type: fixed
diff --git a/changelogs/unreleased/63-nudge-users-to-select-a-template-to-set-up-a-pipeline.yml b/changelogs/unreleased/63-nudge-users-to-select-a-template-to-set-up-a-pipeline.yml
new file mode 100644
index 00000000000..57801ee4ef0
--- /dev/null
+++ b/changelogs/unreleased/63-nudge-users-to-select-a-template-to-set-up-a-pipeline.yml
@@ -0,0 +1,5 @@
+---
+title: Nudge users to select a gitlab-ci.yml template
+merge_request: 24622
+author:
+type: added
diff --git a/changelogs/unreleased/jswain_update_renewal_link.yml b/changelogs/unreleased/jswain_update_renewal_link.yml
new file mode 100644
index 00000000000..aec30d9fdbf
--- /dev/null
+++ b/changelogs/unreleased/jswain_update_renewal_link.yml
@@ -0,0 +1,5 @@
+---
+title: Update renewal banner link for clearer instructions
+merge_request: 26240
+author:
+type: changed
diff --git a/changelogs/unreleased/leipert-polyfills-improvements.yml b/changelogs/unreleased/leipert-polyfills-improvements.yml
new file mode 100644
index 00000000000..25c27ba60c6
--- /dev/null
+++ b/changelogs/unreleased/leipert-polyfills-improvements.yml
@@ -0,0 +1,5 @@
+---
+title: Polyfill fetch for Internet Explorer 11
+merge_request: 26366
+author:
+type: fixed
diff --git a/changelogs/unreleased/revert-e0613e64.yml b/changelogs/unreleased/revert-e0613e64.yml
new file mode 100644
index 00000000000..e94f1df2f4b
--- /dev/null
+++ b/changelogs/unreleased/revert-e0613e64.yml
@@ -0,0 +1,5 @@
+---
+title: Show CI status in project dashboards
+merge_request: 26403
+author:
+type: fixed
diff --git a/changelogs/unreleased/tokenize-filtered-search.yml b/changelogs/unreleased/tokenize-filtered-search.yml
new file mode 100644
index 00000000000..517d2dbe63c
--- /dev/null
+++ b/changelogs/unreleased/tokenize-filtered-search.yml
@@ -0,0 +1,5 @@
+---
+title: Use colon to tokenize input in filtered search
+merge_request: 26072
+author:
+type: changed
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index d3b7fea73c9..aaa1618b1ca 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -484,6 +484,9 @@ msgstr ""
msgid "%{title} changes"
msgstr ""
+msgid "%{token}..."
+msgstr ""
+
msgid "%{totalWeight} total weight"
msgstr ""
@@ -1281,6 +1284,9 @@ msgstr ""
msgid "Admin notes"
msgstr ""
+msgid "AdminArea|Bots"
+msgstr ""
+
msgid "AdminArea|Included Free in license"
msgstr ""
@@ -24074,6 +24080,12 @@ msgstr ""
msgid "success"
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"
msgstr ""
diff --git a/package.json b/package.json
index adb86e11545..3eb5bf5c9a6 100644
--- a/package.json
+++ b/package.json
@@ -125,6 +125,7 @@
"tiptap-commands": "^1.4.0",
"tiptap-extensions": "^1.8.0",
"underscore": "^1.9.0",
+ "unfetch": "^4.1.0",
"url-loader": "^2.1.0",
"visibilityjs": "^1.2.4",
"vue": "^2.6.10",
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index 6bcadda6523..ca2fd2f2e9e 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -57,7 +57,7 @@ describe "Admin Runners" do
expect(page).to have_content 'runner-active'
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).not_to have_content 'runner-paused'
end
@@ -68,7 +68,7 @@ describe "Admin Runners" do
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-paused'
@@ -83,12 +83,12 @@ describe "Admin Runners" do
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-b-1'
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).not_to have_content 'runner-b-1'
expect(page).not_to have_content 'runner-a-2'
@@ -105,7 +105,7 @@ describe "Admin Runners" do
expect(page).to have_content 'runner-project'
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).not_to have_content 'runner-group'
end
@@ -116,7 +116,7 @@ describe "Admin Runners" do
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-group'
@@ -131,12 +131,12 @@ describe "Admin Runners" do
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-b-1'
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).not_to have_content 'runner-b-1'
expect(page).not_to have_content 'runner-a-2'
@@ -153,7 +153,7 @@ describe "Admin Runners" do
expect(page).to have_content 'runner-blue'
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).not_to have_content 'runner-red'
@@ -165,7 +165,7 @@ describe "Admin Runners" do
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'
@@ -179,13 +179,13 @@ describe "Admin Runners" do
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-b-1'
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).not_to have_content 'runner-b-1'
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 8aad598b843..db6bb639e79 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -624,7 +624,7 @@ describe 'Issue Boards', :js do
end
def set_filter(type, text)
- find('.filtered-search').native.send_keys("#{type}=#{text}")
+ find('.filtered-search').native.send_keys("#{type}:=#{text}")
end
def submit_filter
diff --git a/spec/features/boards/modal_filter_spec.rb b/spec/features/boards/modal_filter_spec.rb
index d14041ecf3f..31f4c502c61 100644
--- a/spec/features/boards/modal_filter_spec.rb
+++ b/spec/features/boards/modal_filter_spec.rb
@@ -211,7 +211,7 @@ describe 'Issue Boards add issue modal filtering', :js do
end
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
def submit_filter
diff --git a/spec/features/dashboard/issues_filter_spec.rb b/spec/features/dashboard/issues_filter_spec.rb
index 8e7fd1f500f..8e2a3d983b1 100644
--- a/spec/features/dashboard/issues_filter_spec.rb
+++ b/spec/features/dashboard/issues_filter_spec.rb
@@ -28,14 +28,14 @@ describe 'Dashboard Issues filtering', :js do
context 'filtering by 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_selector('.issue', count: 1)
end
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_selector('.issue', count: 1)
@@ -63,7 +63,7 @@ describe 'Dashboard Issues filtering', :js do
let!(:label_link) { create(:label_link, label: label, target: issue) }
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
expect(page).to have_content issue.title
diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb
index a2ead1b5d33..ff661014fb9 100644
--- a/spec/features/dashboard/issues_spec.rb
+++ b/spec/features/dashboard/issues_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe 'Dashboard Issues' do
it 'shows issues when current user is author', :js do
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_on_public_project.title)
diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb
index bb515cfae82..0c728ab22de 100644
--- a/spec/features/dashboard/merge_requests_spec.rb
+++ b/spec/features/dashboard/merge_requests_spec.rb
@@ -107,7 +107,7 @@ describe 'Dashboard Merge Requests' do
it 'shows authored merge requests', :js do
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_from_fork.title)
@@ -120,7 +120,7 @@ describe 'Dashboard Merge Requests' do
it 'shows labeled merge requests', :js do
reset_filters
- input_filtered_search("label=#{label.name}")
+ input_filtered_search("label:=#{label.name}")
expect(page).to have_content(labeled_merge_request.title)
diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb
index 9bd2e85e3b8..73f759f8a54 100644
--- a/spec/features/dashboard/projects_spec.rb
+++ b/spec/features/dashboard/projects_spec.rb
@@ -152,6 +152,61 @@ describe 'Dashboard Projects' do
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
before do
event = create(:push_event, project: project, author: user)
diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb
index a3fa87e3242..5b2e98804b0 100644
--- a/spec/features/groups/issues_spec.rb
+++ b/spec/features/groups/issues_spec.rb
@@ -48,7 +48,7 @@ describe 'Group issues page' do
let(:user2) { user_outside_group }
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')).not_to have_content(user2.name)
diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb
index 0038a8e4892..f87fa10e2f4 100644
--- a/spec/features/groups/merge_requests_spec.rb
+++ b/spec/features/groups/merge_requests_spec.rb
@@ -52,7 +52,7 @@ describe 'Group merge requests page' do
let(:user2) { user_outside_group }
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')).not_to have_content(user2.name)
diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
index 8aa29cddd5f..c207e91f02e 100644
--- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
@@ -20,13 +20,13 @@ describe 'Dropdown assignee', :js do
describe 'behavior' 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)
end
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)
end
@@ -35,7 +35,7 @@ describe 'Dropdown assignee', :js do
describe 'selecting from dropdown without Ajax call' do
before do
Gitlab::Testing::RequestBlockerMiddleware.block_requests!
- input_filtered_search('assignee=', submit: false, extra_space: false)
+ input_filtered_search('assignee:=', submit: false, extra_space: false)
end
after do
diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb
index c95bd7071b3..8ded11b3b08 100644
--- a/spec/features/issues/filtered_search/dropdown_author_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb
@@ -20,13 +20,13 @@ describe 'Dropdown author', :js do
describe 'behavior' 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)
end
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)
end
@@ -35,7 +35,7 @@ describe 'Dropdown author', :js do
describe 'selecting from dropdown without Ajax call' do
before do
Gitlab::Testing::RequestBlockerMiddleware.block_requests!
- input_filtered_search('author=', submit: false, extra_space: false)
+ input_filtered_search('author:=', submit: false, extra_space: false)
end
after do
diff --git a/spec/features/issues/filtered_search/dropdown_base_spec.rb b/spec/features/issues/filtered_search/dropdown_base_spec.rb
index 2a800f054a0..14d3f48b8fc 100644
--- a/spec/features/issues/filtered_search/dropdown_base_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_base_spec.rb
@@ -27,14 +27,14 @@ describe 'Dropdown base', :js do
it 'shows loading indicator when opened' do
slow_requests do
# 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)
end
end
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')
end
@@ -42,7 +42,7 @@ describe 'Dropdown base', :js do
describe 'caching requests' 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
expect(initial_size).to be > 0
@@ -50,7 +50,7 @@ describe 'Dropdown base', :js do
new_user = create(:user)
project.add_maintainer(new_user)
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)
end
diff --git a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
index 4c11f83318b..9ab0f49cd15 100644
--- a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
@@ -43,19 +43,19 @@ describe 'Dropdown emoji', :js do
describe 'behavior' 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)
end
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)
end
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)
end
diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb
index 1e90efc8d56..a982053dbcb 100644
--- a/spec/features/issues/filtered_search/dropdown_label_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb
@@ -21,7 +21,7 @@ describe 'Dropdown label', :js do
describe 'behavior' do
it 'loads all the labels when opened' do
create(:label, project: project, title: 'bug-label')
- filtered_search.set('label=')
+ filtered_search.set('label:=')
expect_filtered_search_dropdown_results(filter_dropdown, 1)
end
diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
index 1f62a8e0c8d..56beb35a1c5 100644
--- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
@@ -23,7 +23,7 @@ describe 'Dropdown milestone', :js do
describe 'behavior' do
before do
- filtered_search.set('milestone=')
+ filtered_search.set('milestone:=')
end
it 'loads all the milestones when opened' do
diff --git a/spec/features/issues/filtered_search/dropdown_release_spec.rb b/spec/features/issues/filtered_search/dropdown_release_spec.rb
index fd0a98f9ddc..ae1c84d71b4 100644
--- a/spec/features/issues/filtered_search/dropdown_release_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_release_spec.rb
@@ -23,7 +23,7 @@ describe 'Dropdown release', :js do
describe 'behavior' do
before do
- filtered_search.set('release=')
+ filtered_search.set('release:=')
end
it 'loads all the releases when opened' do
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index a518831ea2b..756699fb854 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -67,7 +67,7 @@ describe 'Filter issues', :js do
it 'filters by all available tokens' do
search_term = 'issue'
- input_filtered_search("assignee=@#{user.username} author=@#{user.username} label=~#{caps_sensitive_label.title} milestone=%#{milestone.title} #{search_term}")
+ input_filtered_search("assignee:=@#{user.username} author:=@#{user.username} label:=~#{caps_sensitive_label.title} milestone:=%#{milestone.title} #{search_term}")
wait_for_requests
@@ -84,7 +84,7 @@ describe 'Filter issues', :js do
describe 'filter issues by author' do
context 'only author' do
it 'filters issues by searched author' do
- input_filtered_search("author=@#{user.username}")
+ input_filtered_search("author:=@#{user.username}")
wait_for_requests
@@ -98,7 +98,7 @@ describe 'Filter issues', :js do
describe 'filter issues by assignee' do
context 'only assignee' do
it 'filters issues by searched assignee' do
- input_filtered_search("assignee=@#{user.username}")
+ input_filtered_search("assignee:=@#{user.username}")
wait_for_requests
@@ -108,7 +108,7 @@ describe 'Filter issues', :js do
end
it 'filters issues by no assignee' do
- input_filtered_search('assignee=none')
+ input_filtered_search('assignee:=none')
expect_tokens([assignee_token('None')])
expect_issues_list_count(3)
@@ -122,7 +122,7 @@ describe 'Filter issues', :js do
it 'filters issues by multiple assignees' do
create(:issue, project: project, author: user, assignees: [user2, user])
- input_filtered_search("assignee=@#{user.username} assignee=@#{user2.username}")
+ input_filtered_search("assignee:=@#{user.username} assignee:=@#{user2.username}")
expect_tokens([
assignee_token(user.name),
@@ -138,7 +138,7 @@ describe 'Filter issues', :js do
describe 'filter issues by label' do
context 'only label' do
it 'filters issues by searched label' do
- input_filtered_search("label=~#{bug_label.title}")
+ input_filtered_search("label:=~#{bug_label.title}")
expect_tokens([label_token(bug_label.title)])
expect_issues_list_count(2)
@@ -146,7 +146,7 @@ describe 'Filter issues', :js do
end
it 'filters issues not containing searched label' do
- input_filtered_search("label!=~#{bug_label.title}")
+ input_filtered_search("label:!=~#{bug_label.title}")
expect_tokens([label_token(bug_label.title)])
expect_issues_list_count(6)
@@ -154,7 +154,7 @@ describe 'Filter issues', :js do
end
it 'filters issues by no label' do
- input_filtered_search('label=none')
+ input_filtered_search('label:=none')
expect_tokens([label_token('None', false)])
expect_issues_list_count(4)
@@ -162,7 +162,7 @@ describe 'Filter issues', :js do
end
it 'filters issues by no label' do
- input_filtered_search('label!=none')
+ input_filtered_search('label:!=none')
expect_tokens([label_token('None', false)])
expect_issues_list_count(4)
@@ -170,7 +170,7 @@ describe 'Filter issues', :js do
end
it 'filters issues by multiple labels' do
- input_filtered_search("label=~#{bug_label.title} label=~#{caps_sensitive_label.title}")
+ input_filtered_search("label:=~#{bug_label.title} label:=~#{caps_sensitive_label.title}")
expect_tokens([
label_token(bug_label.title),
@@ -181,7 +181,7 @@ describe 'Filter issues', :js do
end
it 'filters issues by multiple labels with not operator' do
- input_filtered_search("label!=~#{bug_label.title} label=~#{caps_sensitive_label.title}")
+ input_filtered_search("label:!=~#{bug_label.title} label:=~#{caps_sensitive_label.title}")
expect_tokens([
label_token(bug_label.title),
@@ -196,7 +196,7 @@ describe 'Filter issues', :js do
special_issue = create(:issue, title: "Issue with special character label", project: project)
special_issue.labels << special_label
- input_filtered_search("label=~#{special_label.title}")
+ input_filtered_search("label:=~#{special_label.title}")
expect_tokens([label_token(special_label.title)])
expect_issues_list_count(1)
@@ -208,7 +208,7 @@ describe 'Filter issues', :js do
special_issue = create(:issue, title: "Issue with special character label", project: project)
special_issue.labels << special_label
- input_filtered_search("label!=~#{special_label.title}")
+ input_filtered_search("label:!=~#{special_label.title}")
expect_tokens([label_token(special_label.title)])
expect_issues_list_count(8)
@@ -218,7 +218,7 @@ describe 'Filter issues', :js do
it 'does not show issues for unused labels' do
new_label = create(:label, project: project, title: 'new_label')
- input_filtered_search("label=~#{new_label.title}")
+ input_filtered_search("label:=~#{new_label.title}")
expect_tokens([label_token(new_label.title)])
expect_no_issues_list
@@ -226,7 +226,7 @@ describe 'Filter issues', :js do
end
it 'does show issues for bug label' do
- input_filtered_search("label!=~#{bug_label.title}")
+ input_filtered_search("label:!=~#{bug_label.title}")
expect_tokens([label_token(bug_label.title)])
expect_issues_list_count(6)
@@ -240,7 +240,7 @@ describe 'Filter issues', :js do
special_multiple_issue = create(:issue, title: "Issue with special character multiple words label", project: project)
special_multiple_issue.labels << special_multiple_label
- input_filtered_search("label=~'#{special_multiple_label.title}'")
+ input_filtered_search("label:=~'#{special_multiple_label.title}'")
# Check for search results (which makes sure that the page has changed)
expect_issues_list_count(1)
@@ -252,7 +252,7 @@ describe 'Filter issues', :js do
end
it 'single quotes' do
- input_filtered_search("label=~'#{multiple_words_label.title}'")
+ input_filtered_search("label:=~'#{multiple_words_label.title}'")
expect_issues_list_count(1)
expect_tokens([label_token("\"#{multiple_words_label.title}\"")])
@@ -260,7 +260,7 @@ describe 'Filter issues', :js do
end
it 'double quotes' do
- input_filtered_search("label=~\"#{multiple_words_label.title}\"")
+ input_filtered_search("label:=~\"#{multiple_words_label.title}\"")
expect_tokens([label_token("\"#{multiple_words_label.title}\"")])
expect_issues_list_count(1)
@@ -272,7 +272,7 @@ describe 'Filter issues', :js do
double_quotes_label_issue = create(:issue, title: "Issue with double quotes label", project: project)
double_quotes_label_issue.labels << double_quotes_label
- input_filtered_search("label=~'#{double_quotes_label.title}'")
+ input_filtered_search("label:=~'#{double_quotes_label.title}'")
expect_tokens([label_token("'#{double_quotes_label.title}'")])
expect_issues_list_count(1)
@@ -284,7 +284,7 @@ describe 'Filter issues', :js do
single_quotes_label_issue = create(:issue, title: "Issue with single quotes label", project: project)
single_quotes_label_issue.labels << single_quotes_label
- input_filtered_search("label=~\"#{single_quotes_label.title}\"")
+ input_filtered_search("label:=~\"#{single_quotes_label.title}\"")
expect_tokens([label_token("\"#{single_quotes_label.title}\"")])
expect_issues_list_count(1)
@@ -296,7 +296,7 @@ describe 'Filter issues', :js do
it 'filters issues by searched label, label2, author, assignee, milestone and text' do
search_term = 'bug'
- input_filtered_search("label=~#{bug_label.title} label=~#{caps_sensitive_label.title} author=@#{user.username} assignee=@#{user.username} milestone=%#{milestone.title} #{search_term}")
+ input_filtered_search("label:=~#{bug_label.title} label:=~#{caps_sensitive_label.title} author:=@#{user.username} assignee:=@#{user.username} milestone:=%#{milestone.title} #{search_term}")
wait_for_requests
@@ -314,7 +314,7 @@ describe 'Filter issues', :js do
it 'filters issues by searched label, label2, author, assignee, not included in a milestone' do
search_term = 'bug'
- input_filtered_search("label=~#{bug_label.title} label=~#{caps_sensitive_label.title} author=@#{user.username} assignee=@#{user.username} milestone!=%#{milestone.title} #{search_term}")
+ input_filtered_search("label:=~#{bug_label.title} label:=~#{caps_sensitive_label.title} author:=@#{user.username} assignee:=@#{user.username} milestone:!=%#{milestone.title} #{search_term}")
wait_for_requests
@@ -344,7 +344,7 @@ describe 'Filter issues', :js do
describe 'filter issues by milestone' do
context 'only milestone' do
it 'filters issues by searched milestone' do
- input_filtered_search("milestone=%#{milestone.title}")
+ input_filtered_search("milestone:=%#{milestone.title}")
expect_tokens([milestone_token(milestone.title)])
expect_issues_list_count(5)
@@ -352,7 +352,7 @@ describe 'Filter issues', :js do
end
it 'filters issues by no milestone' do
- input_filtered_search("milestone=none")
+ input_filtered_search("milestone:=none")
expect_tokens([milestone_token('None', false)])
expect_issues_list_count(3)
@@ -360,7 +360,7 @@ describe 'Filter issues', :js do
end
it 'filters issues by negation of no milestone' do
- input_filtered_search("milestone!=none ")
+ input_filtered_search("milestone:!=none ")
expect_tokens([milestone_token('None', false, '!=')])
expect_issues_list_count(5)
@@ -372,7 +372,7 @@ describe 'Filter issues', :js do
create(:issue, project: project, milestone: future_milestone, author: user)
end
- input_filtered_search("milestone=upcoming")
+ input_filtered_search("milestone:=upcoming")
expect_tokens([milestone_token('Upcoming', false)])
expect_issues_list_count(1)
@@ -384,7 +384,7 @@ describe 'Filter issues', :js do
create(:issue, project: project, milestone: future_milestone, author: user)
end
- input_filtered_search("milestone!=upcoming")
+ input_filtered_search("milestone:!=upcoming")
expect_tokens([milestone_token('Upcoming', false, '!=')])
expect_issues_list_count(8)
@@ -392,7 +392,7 @@ describe 'Filter issues', :js do
end
it 'filters issues by started milestones' do
- input_filtered_search("milestone=started")
+ input_filtered_search("milestone:=started")
expect_tokens([milestone_token('Started', false)])
expect_issues_list_count(5)
@@ -400,7 +400,7 @@ describe 'Filter issues', :js do
end
it 'filters issues by negation of started milestones' do
- input_filtered_search("milestone!=started")
+ input_filtered_search("milestone:!=started")
expect_tokens([milestone_token('Started', false, '!=')])
expect_issues_list_count(3)
@@ -411,7 +411,7 @@ describe 'Filter issues', :js do
special_milestone = create(:milestone, title: '!@\#{$%^&*()}', project: project)
create(:issue, project: project, milestone: special_milestone)
- input_filtered_search("milestone=%#{special_milestone.title}")
+ input_filtered_search("milestone:=%#{special_milestone.title}")
expect_tokens([milestone_token(special_milestone.title)])
expect_issues_list_count(1)
@@ -422,7 +422,7 @@ describe 'Filter issues', :js do
special_milestone = create(:milestone, title: '!@\#{$%^&*()}', project: project)
create(:issue, project: project, milestone: special_milestone)
- input_filtered_search("milestone!=%#{special_milestone.title}")
+ input_filtered_search("milestone:!=%#{special_milestone.title}")
expect_tokens([milestone_token(special_milestone.title, false, '!=')])
expect_issues_list_count(8)
@@ -432,7 +432,7 @@ describe 'Filter issues', :js do
it 'does not show issues for unused milestones' do
new_milestone = create(:milestone, title: 'new', project: project)
- input_filtered_search("milestone=%#{new_milestone.title}")
+ input_filtered_search("milestone:=%#{new_milestone.title}")
expect_tokens([milestone_token(new_milestone.title)])
expect_no_issues_list
@@ -442,7 +442,7 @@ describe 'Filter issues', :js do
it 'show issues for unused milestones' do
new_milestone = create(:milestone, title: 'new', project: project)
- input_filtered_search("milestone!=%#{new_milestone.title}")
+ input_filtered_search("milestone:!=%#{new_milestone.title}")
expect_tokens([milestone_token(new_milestone.title, false, '!=')])
expect_issues_list_count(8)
@@ -521,7 +521,7 @@ describe 'Filter issues', :js do
context 'searched text with other filters' do
it 'filters issues by searched text, author, text, assignee, text, label1, text, label2, text, milestone and text' do
- input_filtered_search("bug author=@#{user.username} report label=~#{bug_label.title} label=~#{caps_sensitive_label.title} milestone=%#{milestone.title} foo")
+ input_filtered_search("bug author:=@#{user.username} report label:=~#{bug_label.title} label:=~#{caps_sensitive_label.title} milestone:=%#{milestone.title} foo")
expect_issues_list_count(1)
expect_filtered_search_input('bug report foo')
@@ -595,7 +595,7 @@ describe 'Filter issues', :js do
end
it 'milestone dropdown loads milestones' do
- input_filtered_search("milestone=", submit: false)
+ input_filtered_search("milestone:=", submit: false)
within('#js-dropdown-milestone') do
expect(page).to have_selector('.filter-dropdown .filter-dropdown-item', count: 1)
@@ -603,7 +603,7 @@ describe 'Filter issues', :js do
end
it 'label dropdown load labels' do
- input_filtered_search("label=", submit: false)
+ input_filtered_search("label:=", submit: false)
within('#js-dropdown-label') do
expect(page).to have_selector('.filter-dropdown .filter-dropdown-item', count: 3)
diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb
index 2af2e096bcc..29111bff344 100644
--- a/spec/features/issues/filtered_search/visual_tokens_spec.rb
+++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb
@@ -36,7 +36,7 @@ describe 'Visual tokens', :js do
describe 'editing a single token' 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
wait_for_requests
end
@@ -77,7 +77,7 @@ describe 'Visual tokens', :js do
describe 'editing multiple tokens' 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
end
@@ -93,7 +93,7 @@ describe 'Visual tokens', :js do
describe 'editing a search term while editing another filter token' do
before do
- input_filtered_search('foo assignee=', submit: false)
+ input_filtered_search('foo assignee:=', submit: false)
first('.tokens-container .filtered-search-term').click
end
@@ -112,7 +112,7 @@ describe 'Visual tokens', :js do
describe 'add new token after editing existing token' 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
filtered_search.send_keys(' ')
end
@@ -123,7 +123,7 @@ describe 'Visual tokens', :js do
end
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)
end
@@ -131,7 +131,7 @@ describe 'Visual tokens', :js do
describe 'visual tokens' 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]
expect(token.find('.name').text).to eq('Author')
@@ -140,7 +140,7 @@ describe 'Visual tokens', :js do
end
it 'does not tokenize incomplete token' do
- filtered_search.send_keys('author=')
+ filtered_search.send_keys('author:=')
find('body').click
token = page.all('.tokens-container .js-visual-token')[1]
@@ -152,7 +152,7 @@ describe 'Visual tokens', :js do
describe 'search using incomplete visual tokens' do
before do
- input_filtered_search('author=@root assignee=none', extra_space: false)
+ input_filtered_search('author:=@root assignee:=none', extra_space: false)
end
it 'tokenizes the search term to complete visual token' do
diff --git a/spec/features/merge_requests/filters_generic_behavior_spec.rb b/spec/features/merge_requests/filters_generic_behavior_spec.rb
index c3400acae4f..2bea819cc33 100644
--- a/spec/features/merge_requests/filters_generic_behavior_spec.rb
+++ b/spec/features/merge_requests/filters_generic_behavior_spec.rb
@@ -23,7 +23,7 @@ describe 'Merge Requests > Filters generic behavior', :js do
context 'when filtered by a label' do
before do
- input_filtered_search('label=~bug')
+ input_filtered_search('label:=~bug')
end
describe 'state tabs' do
diff --git a/spec/features/merge_requests/user_filters_by_assignees_spec.rb b/spec/features/merge_requests/user_filters_by_assignees_spec.rb
index 3abee3b656a..12d682bbb15 100644
--- a/spec/features/merge_requests/user_filters_by_assignees_spec.rb
+++ b/spec/features/merge_requests/user_filters_by_assignees_spec.rb
@@ -18,7 +18,7 @@ describe 'Merge Requests > User filters by assignees', :js do
context 'filtering by assignee:none' 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).not_to have_content 'Bugfix1'
@@ -28,7 +28,7 @@ describe 'Merge Requests > User filters by assignees', :js do
context 'filtering by assignee=@username' 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_content 'Bugfix1'
diff --git a/spec/features/merge_requests/user_filters_by_labels_spec.rb b/spec/features/merge_requests/user_filters_by_labels_spec.rb
index 7a80ebe9be3..6308579d2d9 100644
--- a/spec/features/merge_requests/user_filters_by_labels_spec.rb
+++ b/spec/features/merge_requests/user_filters_by_labels_spec.rb
@@ -22,7 +22,7 @@ describe 'Merge Requests > User filters by labels', :js do
context 'filtering by label:none' 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).not_to have_content 'Bugfix1'
@@ -32,7 +32,7 @@ describe 'Merge Requests > User filters by labels', :js do
context 'filtering by label:~enhancement' 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_content 'Bugfix2'
@@ -42,7 +42,7 @@ describe 'Merge Requests > User filters by labels', :js do
context 'filtering by label:~enhancement and label:~bug' 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_content 'Bugfix2'
diff --git a/spec/features/merge_requests/user_filters_by_milestones_spec.rb b/spec/features/merge_requests/user_filters_by_milestones_spec.rb
index 8cb686e191e..d2a420be996 100644
--- a/spec/features/merge_requests/user_filters_by_milestones_spec.rb
+++ b/spec/features/merge_requests/user_filters_by_milestones_spec.rb
@@ -18,14 +18,14 @@ describe 'Merge Requests > User filters by milestones', :js do
end
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_css('.merge-request', count: 1)
end
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_css('.merge-request', count: 1)
@@ -33,7 +33,7 @@ describe 'Merge Requests > User filters by milestones', :js do
describe 'filters by upcoming milestone' 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_css('.merge-request', count: 0)
@@ -43,7 +43,7 @@ describe 'Merge Requests > User filters by milestones', :js do
let(:milestone) { create(:milestone, project: project, due_date: Date.tomorrow) }
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_css('.merge-request', count: 1)
@@ -54,7 +54,7 @@ describe 'Merge Requests > User filters by milestones', :js do
let(:milestone) { create(:milestone, project: project, due_date: Date.yesterday) }
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_css('.merge-request', count: 0)
diff --git a/spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb b/spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb
index 5c9d53778d2..5fac31e58ba 100644
--- a/spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb
+++ b/spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb
@@ -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
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_content 'Bugfix2'
@@ -30,7 +30,7 @@ describe 'Merge requests > User filters by multiple criteria', :js do
describe 'filtering 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_content 'Bugfix2'
diff --git a/spec/features/merge_requests/user_filters_by_target_branch_spec.rb b/spec/features/merge_requests/user_filters_by_target_branch_spec.rb
index faff7de729d..abe97d4c07e 100644
--- a/spec/features/merge_requests/user_filters_by_target_branch_spec.rb
+++ b/spec/features/merge_requests/user_filters_by_target_branch_spec.rb
@@ -17,7 +17,7 @@ describe 'Merge Requests > User filters by target branch', :js do
context 'filtering by target-branch:master' 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_content mr1.title
@@ -27,7 +27,7 @@ describe 'Merge Requests > User filters by target branch', :js do
context 'filtering by target-branch:merged-target' 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).not_to have_content mr1.title
@@ -37,7 +37,7 @@ describe 'Merge Requests > User filters by target branch', :js do
context 'filtering by target-branch:feature' 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).not_to have_content mr1.title
diff --git a/spec/features/projects/blobs/user_follows_pipeline_suggest_nudge_spec.rb b/spec/features/projects/blobs/user_follows_pipeline_suggest_nudge_spec.rb
new file mode 100644
index 00000000000..b23cea65b37
--- /dev/null
+++ b/spec/features/projects/blobs/user_follows_pipeline_suggest_nudge_spec.rb
@@ -0,0 +1,67 @@
+# 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
diff --git a/spec/features/projects/releases/user_views_edit_release_spec.rb b/spec/features/projects/releases/user_views_edit_release_spec.rb
index d4c88065b90..820e8277af3 100644
--- a/spec/features/projects/releases/user_views_edit_release_spec.rb
+++ b/spec/features/projects/releases/user_views_edit_release_spec.rb
@@ -6,22 +6,27 @@ describe 'User edits Release', :js do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:release) { create(:release, project: project, name: 'The first release' ) }
let_it_be(:user) { create(:user) }
+ let(:show_feature_flag) { true }
before do
+ stub_feature_flags(release_show_page: show_feature_flag)
+
project.add_developer(user)
gitlab_sign_in(user)
visit edit_project_release_path(project, release)
+
+ wait_for_requests
end
def fill_out_form_and_click(button_to_click)
fill_in 'Release title', with: 'Updated Release title'
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
it 'renders the breadcrumbs' do
@@ -42,31 +47,66 @@ describe 'User edits Release', :js do
expect(find_field('Release notes').value).to eq(release.description)
expect(page).to have_button('Save changes')
- expect(page).to have_button('Cancel')
+ expect(page).to have_link('Cancel')
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_description = release.description
fill_out_form_and_click 'Cancel'
- expect(current_path).to eq(project_releases_path(project))
-
release.reload
expect(release.name).to eq(original_name)
expect(release.description).to eq(original_description)
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'
- expect(current_path).to eq(project_releases_path(project))
-
release.reload
expect(release.name).to eq('Updated Release title')
expect(release.description).to eq('Updated Release notes')
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
diff --git a/spec/features/projects/releases/user_views_release_spec.rb b/spec/features/projects/releases/user_views_release_spec.rb
new file mode 100644
index 00000000000..6120acb4f1f
--- /dev/null
+++ b/spec/features/projects/releases/user_views_release_spec.rb
@@ -0,0 +1,37 @@
+# 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
diff --git a/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js b/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js
new file mode 100644
index 00000000000..0170ef927cf
--- /dev/null
+++ b/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js
@@ -0,0 +1,46 @@
+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);
+ });
+ });
+});
diff --git a/spec/frontend/monitoring/components/charts/column_spec.js b/spec/frontend/monitoring/components/charts/column_spec.js
index d6a96ffbd65..f368cb7916c 100644
--- a/spec/frontend/monitoring/components/charts/column_spec.js
+++ b/spec/frontend/monitoring/components/charts/column_spec.js
@@ -6,56 +6,75 @@ jest.mock('~/lib/utils/icon_utils', () => ({
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', () => {
- let columnChart;
+ let wrapper;
+
+ const findChart = () => wrapper.find(GlColumnChart);
+ const chartProps = prop => findChart().props(prop);
beforeEach(() => {
- columnChart = shallowMount(ColumnChart, {
+ wrapper = shallowMount(ColumnChart, {
propsData: {
graphData: {
+ yAxis: {
+ name: yAxisName,
+ format: yAxisFormat,
+ precision: yAxisPrecistion,
+ },
metrics: [
{
- x_label: 'Time',
- y_label: 'Usage',
result: [
{
metric: {},
- values: [
- [1495700554.925, '8.0390625'],
- [1495700614.925, '8.0390625'],
- [1495700674.925, '8.0390625'],
- ],
+ values: dataValues,
},
],
},
],
},
- containerWidth: 100,
},
});
});
afterEach(() => {
- columnChart.destroy();
+ wrapper.destroy();
});
describe('wrapped components', () => {
describe('GitLab UI column chart', () => {
- let glColumnChart;
+ it('is a Vue instance', () => {
+ expect(findChart().isVueInstance()).toBe(true);
+ });
- beforeEach(() => {
- glColumnChart = columnChart.find(GlColumnChart);
+ it('receives data properties needed for proper chart render', () => {
+ expect(chartProps('data').values).toEqual(dataValues);
});
- it('is a Vue instance', () => {
- expect(glColumnChart.isVueInstance()).toBe(true);
+ it('passes the y axis name correctly', () => {
+ expect(chartProps('yAxisTitle')).toBe(yAxisName);
});
- it('receives data properties needed for proper chart render', () => {
- const props = glColumnChart.props();
+ it('passes the y axis configuration correctly', () => {
+ expect(chartProps('option').yAxis).toMatchObject({
+ name: yAxisName,
+ axisLabel: {
+ formatter: expect.any(Function),
+ },
+ scale: false,
+ });
+ });
- expect(props.data).toBe(columnChart.vm.chartData);
- expect(props.option).toBe(columnChart.vm.chartOptions);
+ it('passes a dataZoom configuration', () => {
+ expect(chartProps('option').dataZoom).toBeDefined();
});
});
});
diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js
index 60b1510973d..47651eca3c8 100644
--- a/spec/frontend/monitoring/mock_data.js
+++ b/spec/frontend/monitoring/mock_data.js
@@ -544,6 +544,12 @@ export const dashboardGitResponse = [
...customDashboardsData,
];
+export const mockDashboardsErrorResponse = {
+ all_dashboards: customDashboardsData,
+ message: "Each 'panel_group' must define an array :panels",
+ status: 'error',
+};
+
export const graphDataPrometheusQuery = {
title: 'Super Chart A2',
type: 'single-stat',
diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js
index 211950facd7..ba41a75ceec 100644
--- a/spec/frontend/monitoring/store/actions_spec.js
+++ b/spec/frontend/monitoring/store/actions_spec.js
@@ -30,6 +30,7 @@ import {
metricsDashboardResponse,
metricsDashboardViewModel,
dashboardGitResponse,
+ mockDashboardsErrorResponse,
} from '../mock_data';
jest.mock('~/flash');
@@ -257,9 +258,11 @@ describe('Monitoring store actions', () => {
describe('fetchDashboard', () => {
let dispatch;
let state;
+ let commit;
const response = metricsDashboardResponse;
beforeEach(() => {
dispatch = jest.fn();
+ commit = jest.fn();
state = storeState();
state.dashboardEndpoint = '/dashboard';
});
@@ -270,6 +273,7 @@ describe('Monitoring store actions', () => {
fetchDashboard(
{
state,
+ commit,
dispatch,
},
params,
@@ -287,19 +291,21 @@ describe('Monitoring store actions', () => {
describe('on failure', () => {
let result;
- let errorResponse;
beforeEach(() => {
const params = {};
result = () => {
- mock.onGet(state.dashboardEndpoint).replyOnce(500, errorResponse);
- return fetchDashboard({ state, dispatch }, params);
+ mock.onGet(state.dashboardEndpoint).replyOnce(500, mockDashboardsErrorResponse);
+ return fetchDashboard({ state, commit, dispatch }, params);
};
});
it('dispatches a failure action', done => {
- errorResponse = {};
result()
.then(() => {
+ expect(commit).toHaveBeenCalledWith(
+ types.SET_ALL_DASHBOARDS,
+ mockDashboardsErrorResponse.all_dashboards,
+ );
expect(dispatch).toHaveBeenCalledWith(
'receiveMetricsDashboardFailure',
new Error('Request failed with status code 500'),
@@ -311,15 +317,15 @@ describe('Monitoring store actions', () => {
});
it('dispatches a failure action when a message is returned', done => {
- const message = 'Something went wrong with Prometheus!';
- errorResponse = { message };
result()
.then(() => {
expect(dispatch).toHaveBeenCalledWith(
'receiveMetricsDashboardFailure',
new Error('Request failed with status code 500'),
);
- expect(createFlash).toHaveBeenCalledWith(expect.stringContaining(message));
+ expect(createFlash).toHaveBeenCalledWith(
+ expect.stringContaining(mockDashboardsErrorResponse.message),
+ );
done();
})
.catch(done.fail);
diff --git a/spec/frontend/releases/components/app_edit_spec.js b/spec/frontend/releases/components/app_edit_spec.js
index b2dbb8cc435..ac4b2b9124f 100644
--- a/spec/frontend/releases/components/app_edit_spec.js
+++ b/spec/frontend/releases/components/app_edit_spec.js
@@ -1,30 +1,27 @@
import Vuex from 'vuex';
import { mount } from '@vue/test-utils';
import ReleaseEditApp from '~/releases/components/app_edit.vue';
-import { release } from '../mock_data';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { release as originalRelease } from '../mock_data';
+import * as commonUtils from '~/lib/utils/common_utils';
+import { BACK_URL_PARAM } from '~/releases/constants';
describe('Release edit component', () => {
let wrapper;
- let releaseClone;
+ let release;
let actions;
let state;
- beforeEach(() => {
- gon.api_version = 'v4';
-
- releaseClone = convertObjectPropsToCamelCase(release, { deep: true });
-
+ const factory = () => {
state = {
- release: releaseClone,
+ release,
markdownDocsPath: 'path/to/markdown/docs',
updateReleaseApiDocsPath: 'path/to/update/release/api/docs',
+ releasesPagePath: 'path/to/releases/page',
};
actions = {
fetchRelease: jest.fn(),
updateRelease: jest.fn(),
- navigateToReleasesPage: jest.fn(),
};
const store = new Vuex.Store({
@@ -40,58 +37,99 @@ describe('Release edit component', () => {
wrapper = mount(ReleaseEditApp, {
store,
});
+ };
- return wrapper.vm.$nextTick();
- });
+ beforeEach(() => {
+ gon.api_version = 'v4';
- it('calls fetchRelease when the component is created', () => {
- expect(actions.fetchRelease).toHaveBeenCalledTimes(1);
+ release = commonUtils.convertObjectPropsToCamelCase(originalRelease, { deep: true });
});
- it('renders the description text at the top of the page', () => {
- expect(wrapper.find('.js-subtitle-text').text()).toBe(
- 'Releases are based on Git tags. We recommend naming tags that fit within semantic versioning, for example v1.0, v2.0-pre.',
- );
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
});
- it('renders the correct tag name in the "Tag name" field', () => {
- expect(wrapper.find('#git-ref').element.value).toBe(releaseClone.tagName);
- });
+ describe(`basic functionality tests: all tests unrelated to the "${BACK_URL_PARAM}" parameter`, () => {
+ beforeEach(() => {
+ factory();
+ });
- it('renders the correct help text under the "Tag name" field', () => {
- const helperText = wrapper.find('#tag-name-help');
- const helperTextLink = helperText.find('a');
- const helperTextLinkAttrs = helperTextLink.attributes();
-
- expect(helperText.text()).toBe(
- 'Changing a Release tag is only supported via Releases API. More information',
- );
- expect(helperTextLink.text()).toBe('More information');
- expect(helperTextLinkAttrs.href).toBe(state.updateReleaseApiDocsPath);
- expect(helperTextLinkAttrs.rel).toContain('noopener');
- expect(helperTextLinkAttrs.rel).toContain('noreferrer');
- expect(helperTextLinkAttrs.target).toBe('_blank');
- });
+ it('calls fetchRelease when the component is created', () => {
+ expect(actions.fetchRelease).toHaveBeenCalledTimes(1);
+ });
- it('renders the correct release title in the "Release title" field', () => {
- expect(wrapper.find('#release-title').element.value).toBe(releaseClone.name);
- });
+ it('renders the description text at the top of the page', () => {
+ expect(wrapper.find('.js-subtitle-text').text()).toBe(
+ 'Releases are based on Git tags. We recommend naming tags that fit within semantic versioning, for example v1.0, v2.0-pre.',
+ );
+ });
- it('renders the release notes in the "Release notes" textarea', () => {
- expect(wrapper.find('#release-notes').element.value).toBe(releaseClone.description);
- });
+ it('renders the correct tag name in the "Tag name" field', () => {
+ expect(wrapper.find('#git-ref').element.value).toBe(release.tagName);
+ });
+
+ it('renders the correct help text under the "Tag name" field', () => {
+ const helperText = wrapper.find('#tag-name-help');
+ const helperTextLink = helperText.find('a');
+ const helperTextLinkAttrs = helperTextLink.attributes();
+
+ expect(helperText.text()).toBe(
+ 'Changing a Release tag is only supported via Releases API. More information',
+ );
+ expect(helperTextLink.text()).toBe('More information');
+ expect(helperTextLinkAttrs).toEqual(
+ expect.objectContaining({
+ href: state.updateReleaseApiDocsPath,
+ rel: 'noopener noreferrer',
+ target: '_blank',
+ }),
+ );
+ });
+
+ it('renders the correct release title in the "Release title" field', () => {
+ expect(wrapper.find('#release-title').element.value).toBe(release.name);
+ });
+
+ it('renders the release notes in the "Release notes" textarea', () => {
+ expect(wrapper.find('#release-notes').element.value).toBe(release.description);
+ });
+
+ it('renders the "Save changes" button as type="submit"', () => {
+ expect(wrapper.find('.js-submit-button').attributes('type')).toBe('submit');
+ });
- it('renders the "Save changes" button as type="submit"', () => {
- expect(wrapper.find('.js-submit-button').attributes('type')).toBe('submit');
+ it('calls updateRelease when the form is submitted', () => {
+ wrapper.find('form').trigger('submit');
+ expect(actions.updateRelease).toHaveBeenCalledTimes(1);
+ });
});
- it('calls updateRelease when the form is submitted', () => {
- wrapper.find('form').trigger('submit');
- 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', () => {
- wrapper.find('.js-cancel-button').vm.$emit('click');
- expect(actions.navigateToReleasesPage).toHaveBeenCalledTimes(1);
+ describe(`when the URL contains a "${BACK_URL_PARAM}" parameter`, () => {
+ const backUrl = 'https://example.gitlab.com/back/url';
+
+ 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);
+ });
});
});
diff --git a/spec/frontend/releases/components/app_show_spec.js b/spec/frontend/releases/components/app_show_spec.js
new file mode 100644
index 00000000000..3dc9964c25c
--- /dev/null
+++ b/spec/frontend/releases/components/app_show_spec.js
@@ -0,0 +1,61 @@
+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);
+ });
+});
diff --git a/spec/frontend/releases/components/release_block_header_spec.js b/spec/frontend/releases/components/release_block_header_spec.js
index 44f6f63fa79..9c6cbc86d3c 100644
--- a/spec/frontend/releases/components/release_block_header_spec.js
+++ b/spec/frontend/releases/components/release_block_header_spec.js
@@ -4,6 +4,7 @@ import { GlLink } from '@gitlab/ui';
import ReleaseBlockHeader from '~/releases/components/release_block_header.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { release as originalRelease } from '../mock_data';
+import { BACK_URL_PARAM } from '~/releases/constants';
describe('Release block header', () => {
let wrapper;
@@ -27,6 +28,7 @@ describe('Release block header', () => {
const findHeader = () => wrapper.find('h2');
const findHeaderLink = () => findHeader().find(GlLink);
+ const findEditButton = () => wrapper.find('.js-edit-button');
describe('when _links.self is provided', () => {
beforeEach(() => {
@@ -51,4 +53,39 @@ describe('Release block header', () => {
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);
+ });
+ });
});
diff --git a/spec/frontend/releases/components/release_block_spec.js b/spec/frontend/releases/components/release_block_spec.js
index ff88e3193bc..227998b0271 100644
--- a/spec/frontend/releases/components/release_block_spec.js
+++ b/spec/frontend/releases/components/release_block_spec.js
@@ -7,20 +7,9 @@ import ReleaseBlockFooter from '~/releases/components/release_block_footer.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { release as originalRelease } from '../mock_data';
import Icon from '~/vue_shared/components/icon.vue';
-import { scrollToElement } from '~/lib/utils/common_utils';
-
-const { convertObjectPropsToCamelCase } = jest.requireActual('~/lib/utils/common_utils');
-
-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(),
-}));
+import * as commonUtils from '~/lib/utils/common_utils';
+import { BACK_URL_PARAM } from '~/releases/constants';
+import * as urlUtility from '~/lib/utils/url_utility';
describe('Release block', () => {
let wrapper;
@@ -47,7 +36,7 @@ describe('Release block', () => {
beforeEach(() => {
jest.spyOn($.fn, 'renderGFM');
- release = convertObjectPropsToCamelCase(originalRelease, { deep: true });
+ release = commonUtils.convertObjectPropsToCamelCase(originalRelease, { deep: true });
});
afterEach(() => {
@@ -61,9 +50,11 @@ describe('Release block', () => {
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().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', () => {
@@ -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', () => {
delete release.milestones;
@@ -203,37 +186,40 @@ describe('Release block', () => {
});
describe('anchor scrolling', () => {
+ let locationHash;
+
beforeEach(() => {
- scrollToElement.mockClear();
+ commonUtils.scrollToElement = jest.fn();
+ urlUtility.getLocationHash = jest.fn().mockImplementation(() => locationHash);
});
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', () => {
- mockLocationHash = '';
+ locationHash = '';
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", () => {
- mockLocationHash = 'v0.4';
+ locationHash = 'v0.4';
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", () => {
- mockLocationHash = release.tagName;
+ locationHash = release.tagName;
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', () => {
- mockLocationHash = release.tagName;
+ locationHash = release.tagName;
return factory(release).then(() => {
expect(hasTargetBlueBackground()).toBe(true);
@@ -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', () => {
- mockLocationHash = '';
+ locationHash = '';
return factory(release).then(() => {
expect(hasTargetBlueBackground()).toBe(false);
diff --git a/spec/frontend/releases/stores/modules/detail/actions_spec.js b/spec/frontend/releases/stores/modules/detail/actions_spec.js
index 0c2763822c9..88346083f5a 100644
--- a/spec/frontend/releases/stores/modules/detail/actions_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/actions_spec.js
@@ -1,13 +1,14 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
+import { cloneDeep, merge } from 'lodash';
import * as actions from '~/releases/stores/modules/detail/actions';
import * as types from '~/releases/stores/modules/detail/mutation_types';
-import { release } from '../../../mock_data';
-import state from '~/releases/stores/modules/detail/state';
+import { release as originalRelease } from '../../../mock_data';
+import createState from '~/releases/stores/modules/detail/state';
import createFlash from '~/flash';
-import { redirectTo } from '~/lib/utils/url_utility';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { redirectTo } from '~/lib/utils/url_utility';
jest.mock('~/flash', () => jest.fn());
@@ -17,14 +18,14 @@ jest.mock('~/lib/utils/url_utility', () => ({
}));
describe('Release detail actions', () => {
- let stateClone;
- let releaseClone;
+ let state;
+ let release;
let mock;
let error;
beforeEach(() => {
- stateClone = state();
- releaseClone = JSON.parse(JSON.stringify(release));
+ state = createState();
+ release = cloneDeep(originalRelease);
mock = new MockAdapter(axios);
gon.api_version = 'v4';
error = { message: 'An error occurred' };
@@ -39,7 +40,7 @@ describe('Release detail actions', () => {
it(`commits ${types.SET_INITIAL_STATE} with the provided object`, () => {
const initialState = {};
- return testAction(actions.setInitialState, initialState, stateClone, [
+ return testAction(actions.setInitialState, initialState, state, [
{ type: types.SET_INITIAL_STATE, payload: initialState },
]);
});
@@ -47,19 +48,19 @@ describe('Release detail actions', () => {
describe('requestRelease', () => {
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', () => {
it(`commits ${types.RECEIVE_RELEASE_SUCCESS}`, () =>
- testAction(actions.receiveReleaseSuccess, releaseClone, stateClone, [
- { type: types.RECEIVE_RELEASE_SUCCESS, payload: releaseClone },
+ testAction(actions.receiveReleaseSuccess, release, state, [
+ { type: types.RECEIVE_RELEASE_SUCCESS, payload: release },
]));
});
describe('receiveReleaseError', () => {
it(`commits ${types.RECEIVE_RELEASE_ERROR}`, () =>
- testAction(actions.receiveReleaseError, error, stateClone, [
+ testAction(actions.receiveReleaseError, error, state, [
{ type: types.RECEIVE_RELEASE_ERROR, payload: error },
]));
@@ -77,24 +78,24 @@ describe('Release detail actions', () => {
let getReleaseUrl;
beforeEach(() => {
- stateClone.projectId = '18';
- stateClone.tagName = 'v1.3';
- getReleaseUrl = `/api/v4/projects/${stateClone.projectId}/releases/${stateClone.tagName}`;
+ state.projectId = '18';
+ state.tagName = 'v1.3';
+ getReleaseUrl = `/api/v4/projects/${state.projectId}/releases/${state.tagName}`;
});
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(
actions.fetchRelease,
undefined,
- stateClone,
+ state,
[],
[
{ type: 'requestRelease' },
{
type: 'receiveReleaseSuccess',
- payload: convertObjectPropsToCamelCase(releaseClone, { deep: true }),
+ payload: convertObjectPropsToCamelCase(release, { deep: true }),
},
],
);
@@ -106,7 +107,7 @@ describe('Release detail actions', () => {
return testAction(
actions.fetchRelease,
undefined,
- stateClone,
+ state,
[],
[{ type: 'requestRelease' }, { type: 'receiveReleaseError', payload: expect.anything() }],
);
@@ -116,7 +117,7 @@ describe('Release detail actions', () => {
describe('updateReleaseTitle', () => {
it(`commits ${types.UPDATE_RELEASE_TITLE} with the updated 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 },
]);
});
@@ -125,7 +126,7 @@ describe('Release detail actions', () => {
describe('updateReleaseNotes', () => {
it(`commits ${types.UPDATE_RELEASE_NOTES} with the updated 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 },
]);
});
@@ -133,25 +134,40 @@ describe('Release detail actions', () => {
describe('requestUpdateRelease', () => {
it(`commits ${types.REQUEST_UPDATE_RELEASE}`, () =>
- testAction(actions.requestUpdateRelease, undefined, stateClone, [
+ testAction(actions.requestUpdateRelease, undefined, state, [
{ type: types.REQUEST_UPDATE_RELEASE },
]));
});
describe('receiveUpdateReleaseSuccess', () => {
it(`commits ${types.RECEIVE_UPDATE_RELEASE_SUCCESS}`, () =>
- testAction(
- actions.receiveUpdateReleaseSuccess,
- undefined,
- stateClone,
- [{ type: types.RECEIVE_UPDATE_RELEASE_SUCCESS }],
- [{ type: 'navigateToReleasesPage' }],
- ));
+ testAction(actions.receiveUpdateReleaseSuccess, undefined, { ...state, featureFlags: {} }, [
+ { type: types.RECEIVE_UPDATE_RELEASE_SUCCESS },
+ ]));
+
+ describe('when the releaseShowPage feature flag is enabled', () => {
+ 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', () => {
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 },
]));
@@ -169,10 +185,10 @@ describe('Release detail actions', () => {
let getReleaseUrl;
beforeEach(() => {
- stateClone.release = releaseClone;
- stateClone.projectId = '18';
- stateClone.tagName = 'v1.3';
- getReleaseUrl = `/api/v4/projects/${stateClone.projectId}/releases/${stateClone.tagName}`;
+ state.release = release;
+ state.projectId = '18';
+ state.tagName = 'v1.3';
+ getReleaseUrl = `/api/v4/projects/${state.projectId}/releases/${state.tagName}`;
});
it(`dispatches requestUpdateRelease and receiveUpdateReleaseSuccess`, () => {
@@ -181,7 +197,7 @@ describe('Release detail actions', () => {
return testAction(
actions.updateRelease,
undefined,
- stateClone,
+ state,
[],
[{ type: 'requestUpdateRelease' }, { type: 'receiveUpdateReleaseSuccess' }],
);
@@ -193,7 +209,7 @@ describe('Release detail actions', () => {
return testAction(
actions.updateRelease,
undefined,
- stateClone,
+ state,
[],
[
{ type: 'requestUpdateRelease' },
@@ -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);
- });
- });
});
diff --git a/spec/helpers/markup_helper_spec.rb b/spec/helpers/markup_helper_spec.rb
index c22e20f0e73..96c8b557625 100644
--- a/spec/helpers/markup_helper_spec.rb
+++ b/spec/helpers/markup_helper_spec.rb
@@ -531,8 +531,10 @@ describe MarkupHelper do
it 'preserves style attribute for a label that can be accessed by current_user' do
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
it 'does not style a label that can not be accessed by current_user' do
@@ -544,6 +546,15 @@ describe MarkupHelper do
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
object = create_object("@#{user.username}, can you look at this?\nHello world\n")
actual = first_line_in_markdown(object, attribute, 100, project: project)
diff --git a/spec/javascripts/releases/components/app_index_spec.js b/spec/javascripts/releases/components/app_index_spec.js
index 962fe9c448d..8809391f135 100644
--- a/spec/javascripts/releases/components/app_index_spec.js
+++ b/spec/javascripts/releases/components/app_index_spec.js
@@ -27,7 +27,7 @@ describe('Releases App ', () => {
};
beforeEach(() => {
- store = createStore({ list: listModule });
+ store = createStore({ modules: { list: listModule } });
releasesPagination = _.range(21).map(index => ({
...convertObjectPropsToCamelCase(release, { deep: true }),
tagName: `${index}.00`,
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index dd1588036b6..66b298bb36f 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -425,16 +425,16 @@ describe Issue do
let(:issue) { create(:issue, title: 'testing-issue') }
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
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
it "does not contain the issue title if confidential" do
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
context 'issue title longer than 100 characters' do
@@ -932,4 +932,33 @@ describe Issue do
end
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
diff --git a/spec/models/resource_weight_event_spec.rb b/spec/models/resource_weight_event_spec.rb
index 11b633e1dcf..8a37883d933 100644
--- a/spec/models/resource_weight_event_spec.rb
+++ b/spec/models/resource_weight_event_spec.rb
@@ -67,7 +67,7 @@ RSpec.describe ResourceWeightEvent, type: :model do
it 'returns the expected id' do
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')
expect(event.discussion_id).to eq('73d167c478')
diff --git a/spec/support/helpers/filtered_search_helpers.rb b/spec/support/helpers/filtered_search_helpers.rb
index c8b7a9251a9..99a5e043825 100644
--- a/spec/support/helpers/filtered_search_helpers.rb
+++ b/spec/support/helpers/filtered_search_helpers.rb
@@ -26,7 +26,7 @@ module FilteredSearchHelpers
# Select a label clicking in the search dropdown instead
# of entering label names on the input.
def select_label_on_dropdown(label_title)
- input_filtered_search("label=", submit: false)
+ input_filtered_search("label:=", submit: false)
within('#js-dropdown-label') do
wait_for_requests
@@ -71,7 +71,7 @@ module FilteredSearchHelpers
end
def init_label_search
- filtered_search.set('label=')
+ filtered_search.set('label:=')
# This ensures the dropdown is shown
expect(find('#js-dropdown-label')).not_to have_css('.filter-dropdown-loading')
end
diff --git a/spec/support/shared_examples/features/issuables_user_dropdown_behaviors_shared_examples.rb b/spec/support/shared_examples/features/issuables_user_dropdown_behaviors_shared_examples.rb
index e0d9b828992..1848b4fffd9 100644
--- a/spec/support/shared_examples/features/issuables_user_dropdown_behaviors_shared_examples.rb
+++ b/spec/support/shared_examples/features/issuables_user_dropdown_behaviors_shared_examples.rb
@@ -13,7 +13,7 @@ RSpec.shared_examples 'issuable user dropdown behaviors' do
it 'only includes members of the project/group' do
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")).not_to have_content(user_not_in_dropdown.name)
diff --git a/yarn.lock b/yarn.lock
index 7b65266462a..7ef43bf902f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -11453,6 +11453,11 @@ underscore@~1.8.3:
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022"
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:
version "1.1.1"
resolved "https://registry.yarnpkg.com/unherit/-/unherit-1.1.1.tgz#132748da3e88eab767e08fabfbb89c5e9d28628c"