diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-03-04 09:08:20 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-03-04 09:08:20 +0000 |
commit | d80f3cd75e700b6e62910865bfd36734644ffa89 (patch) | |
tree | aa2fa2f2b4385854c13591bef8e74924ef661657 | |
parent | be81c1578d65f25edfde8aa550f190b8d3e6d976 (diff) | |
download | gitlab-ce-d80f3cd75e700b6e62910865bfd36734644ffa89.tar.gz |
Add latest changes from gitlab-org/gitlab@master
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" |