Commit e06d0e77 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent f7dae0cd
import $ from 'jquery'; import $ from 'jquery';
import Sortable from 'sortablejs'; import Sortable from 'sortablejs';
import Vue from 'vue'; import Vue from 'vue';
import { GlButtonGroup, GlButton, GlTooltip } from '@gitlab/ui'; import { GlButtonGroup, GlButton, GlLabel, GlTooltip } from '@gitlab/ui';
import isWipLimitsOn from 'ee_else_ce/boards/mixins/is_wip_limits'; import isWipLimitsOn from 'ee_else_ce/boards/mixins/is_wip_limits';
import { s__, __, sprintf } from '~/locale'; import { s__, __, sprintf } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
...@@ -14,6 +14,7 @@ import IssueCount from './issue_count.vue'; ...@@ -14,6 +14,7 @@ import IssueCount from './issue_count.vue';
import boardsStore from '../stores/boards_store'; import boardsStore from '../stores/boards_store';
import { getBoardSortableDefaultOptions, sortableEnd } from '../mixins/sortable_default_options'; import { getBoardSortableDefaultOptions, sortableEnd } from '../mixins/sortable_default_options';
import { ListType } from '../constants'; import { ListType } from '../constants';
import { isScopedLabel } from '~/lib/utils/common_utils';
export default Vue.extend({ export default Vue.extend({
components: { components: {
...@@ -24,6 +25,7 @@ export default Vue.extend({ ...@@ -24,6 +25,7 @@ export default Vue.extend({
GlButtonGroup, GlButtonGroup,
IssueCount, IssueCount,
GlButton, GlButton,
GlLabel,
GlTooltip, GlTooltip,
}, },
directives: { directives: {
...@@ -95,6 +97,9 @@ export default Vue.extend({ ...@@ -95,6 +97,9 @@ export default Vue.extend({
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
return `boards.${this.boardId}.${this.list.type}.${this.list.id}`; return `boards.${this.boardId}.${this.list.type}.${this.list.id}`;
}, },
helpLink() {
return boardsStore.scopedLabels.helpLink;
},
}, },
watch: { watch: {
filter: { filter: {
...@@ -145,6 +150,10 @@ export default Vue.extend({ ...@@ -145,6 +150,10 @@ export default Vue.extend({
} }
}, },
methods: { methods: {
showScopedLabels(label) {
return boardsStore.scopedLabels.enabled && isScopedLabel(label);
},
showNewIssueForm() { showNewIssueForm() {
this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm; this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm;
}, },
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import $ from 'jquery'; import $ from 'jquery';
import Vue from 'vue'; import Vue from 'vue';
import { GlLabel } from '@gitlab/ui';
import Flash from '~/flash'; import Flash from '~/flash';
import { sprintf, __ } from '~/locale'; import { sprintf, __ } from '~/locale';
import Sidebar from '~/right_sidebar'; import Sidebar from '~/right_sidebar';
...@@ -22,6 +23,7 @@ export default Vue.extend({ ...@@ -22,6 +23,7 @@ export default Vue.extend({
components: { components: {
AssigneeTitle, AssigneeTitle,
Assignees, Assignees,
GlLabel,
SidebarEpicsSelect: () => SidebarEpicsSelect: () =>
import('ee_component/sidebar/components/sidebar_item_epics_select.vue'), import('ee_component/sidebar/components/sidebar_item_epics_select.vue'),
RemoveBtn, RemoveBtn,
...@@ -67,6 +69,9 @@ export default Vue.extend({ ...@@ -67,6 +69,9 @@ export default Vue.extend({
selectedLabels() { selectedLabels() {
return this.hasLabels ? this.issue.labels.map(l => l.title).join(',') : ''; return this.hasLabels ? this.issue.labels.map(l => l.title).join(',') : '';
}, },
helpLink() {
return boardsStore.scopedLabels.helpLink;
},
}, },
watch: { watch: {
detail: { detail: {
...@@ -147,8 +152,5 @@ export default Vue.extend({ ...@@ -147,8 +152,5 @@ export default Vue.extend({
showScopedLabels(label) { showScopedLabels(label) {
return boardsStore.scopedLabels.enabled && isScopedLabel(label); return boardsStore.scopedLabels.enabled && isScopedLabel(label);
}, },
helpLink() {
return boardsStore.scopedLabels.helpLink;
},
}, },
}); });
import $ from 'jquery'; import $ from 'jquery';
import _ from 'underscore'; import { isString, mapValues, isNumber, reduce } from 'lodash';
import * as timeago from 'timeago.js'; import * as timeago from 'timeago.js';
import dateFormat from 'dateformat'; import dateFormat from 'dateformat';
import { languageCode, s__, __, n__ } from '../../locale'; import { languageCode, s__, __, n__ } from '../../locale';
...@@ -79,7 +79,7 @@ export const getDayName = date => ...@@ -79,7 +79,7 @@ export const getDayName = date =>
* @returns {String} * @returns {String}
*/ */
export const formatDate = (datetime, format = 'mmm d, yyyy h:MMtt Z') => { export const formatDate = (datetime, format = 'mmm d, yyyy h:MMtt Z') => {
if (_.isString(datetime) && datetime.match(/\d+-\d+\d+ /)) { if (isString(datetime) && datetime.match(/\d+-\d+\d+ /)) {
throw new Error(__('Invalid date')); throw new Error(__('Invalid date'));
} }
return dateFormat(datetime, format); return dateFormat(datetime, format);
...@@ -497,7 +497,7 @@ export const parseSeconds = ( ...@@ -497,7 +497,7 @@ export const parseSeconds = (
let unorderedMinutes = Math.abs(seconds / SECONDS_PER_MINUTE); let unorderedMinutes = Math.abs(seconds / SECONDS_PER_MINUTE);
return _.mapObject(timePeriodConstraints, minutesPerPeriod => { return mapValues(timePeriodConstraints, minutesPerPeriod => {
if (minutesPerPeriod === 0) { if (minutesPerPeriod === 0) {
return 0; return 0;
} }
...@@ -516,7 +516,7 @@ export const parseSeconds = ( ...@@ -516,7 +516,7 @@ export const parseSeconds = (
* If the 'fullNameFormat' param is passed it returns a non condensed string eg '1 week 3 days' * If the 'fullNameFormat' param is passed it returns a non condensed string eg '1 week 3 days'
*/ */
export const stringifyTime = (timeObject, fullNameFormat = false) => { export const stringifyTime = (timeObject, fullNameFormat = false) => {
const reducedTime = _.reduce( const reducedTime = reduce(
timeObject, timeObject,
(memo, unitValue, unitName) => { (memo, unitValue, unitName) => {
const isNonZero = Boolean(unitValue); const isNonZero = Boolean(unitValue);
...@@ -642,7 +642,7 @@ export const dayAfter = date => new Date(newDate(date).setDate(date.getDate() + ...@@ -642,7 +642,7 @@ export const dayAfter = date => new Date(newDate(date).setDate(date.getDate() +
* @return {String} approximated time * @return {String} approximated time
*/ */
export const approximateDuration = (seconds = 0) => { export const approximateDuration = (seconds = 0) => {
if (!_.isNumber(seconds) || seconds < 0) { if (!isNumber(seconds) || seconds < 0) {
return ''; return '';
} }
......
import fuzzaldrinPlus from 'fuzzaldrin-plus'; import fuzzaldrinPlus from 'fuzzaldrin-plus';
import _ from 'underscore';
import sanitize from 'sanitize-html'; import sanitize from 'sanitize-html';
/** /**
...@@ -17,11 +16,11 @@ import sanitize from 'sanitize-html'; ...@@ -17,11 +16,11 @@ import sanitize from 'sanitize-html';
* @param {String} matchSuffix The string to insert at the end of a match * @param {String} matchSuffix The string to insert at the end of a match
*/ */
export default function highlight(string, match = '', matchPrefix = '<b>', matchSuffix = '</b>') { export default function highlight(string, match = '', matchPrefix = '<b>', matchSuffix = '</b>') {
if (_.isUndefined(string) || _.isNull(string)) { if (!string) {
return ''; return '';
} }
if (_.isUndefined(match) || _.isNull(match) || match === '') { if (!match) {
return string; return string;
} }
...@@ -34,7 +33,7 @@ export default function highlight(string, match = '', matchPrefix = '<b>', match ...@@ -34,7 +33,7 @@ export default function highlight(string, match = '', matchPrefix = '<b>', match
return sanitizedValue return sanitizedValue
.split('') .split('')
.map((character, i) => { .map((character, i) => {
if (_.contains(occurrences, i)) { if (occurrences.includes(i)) {
return `${matchPrefix}${character}${matchSuffix}`; return `${matchPrefix}${character}${matchSuffix}`;
} }
......
import _ from 'underscore'; import { isString } from 'lodash';
/** /**
* Adds a , to a string composed by numbers, at every 3 chars. * Adds a , to a string composed by numbers, at every 3 chars.
...@@ -199,7 +199,7 @@ export const splitCamelCase = string => ...@@ -199,7 +199,7 @@ export const splitCamelCase = string =>
* i.e. "My Group / My Subgroup / My Project" * i.e. "My Group / My Subgroup / My Project"
*/ */
export const truncateNamespace = (string = '') => { export const truncateNamespace = (string = '') => {
if (_.isNull(string) || !_.isString(string)) { if (string === null || !isString(string)) {
return ''; return '';
} }
......
/**
* Formats a number as string using `toLocaleString`.
*
* @param {Number} number to be converted
* @param {params} Parameters
* @param {params.fractionDigits} Number of decimal digits
* to display, defaults to using `toLocaleString` defaults.
* @param {params.maxLength} Max output char lenght at the
* expense of precision, if the output is longer than this,
* the formatter switches to using exponential notation.
* @param {params.factor} Value is multiplied by this factor,
* useful for value normalization.
* @returns Formatted value
*/
function formatNumber(
value,
{ fractionDigits = undefined, valueFactor = 1, style = undefined, maxLength = undefined },
) {
if (value === null) {
return '';
}
const num = value * valueFactor;
const formatted = num.toLocaleString(undefined, {
minimumFractionDigits: fractionDigits,
maximumFractionDigits: fractionDigits,
style,
});
if (maxLength !== undefined && formatted.length > maxLength) {
// 123456 becomes 1.23e+8
return num.toExponential(2);
}
return formatted;
}
/**
* Formats a number as a string scaling it up according to units.
*
* While the number is scaled down, the units are scaled up.
*
* @param {Array} List of units of the scale
* @param {Number} unitFactor - Factor of the scale for each
* unit after which the next unit is used scaled.
*/
const scaledFormatter = (units, unitFactor = 1000) => {
if (unitFactor === 0) {
return new RangeError(`unitFactor cannot have the value 0.`);
}
return (value, fractionDigits) => {
if (value === null) {
return '';
}
if (
value === Number.NEGATIVE_INFINITY ||
value === Number.POSITIVE_INFINITY ||
Number.isNaN(value)
) {
return value.toLocaleString(undefined);
}
let num = value;
let scale = 0;
const limit = units.length;
while (Math.abs(num) >= unitFactor) {
scale += 1;
num /= unitFactor;
if (scale >= limit) {
return 'NA';
}
}
const unit = units[scale];
return `${formatNumber(num, { fractionDigits })}${unit}`;
};
};
/**
* Returns a function that formats a number as a string.
*/
export const numberFormatter = (style = 'decimal', valueFactor = 1) => {
return (value, fractionDigits, maxLength) => {
return `${formatNumber(value, { fractionDigits, maxLength, valueFactor, style })}`;
};
};
/**
* Returns a function that formats a number as a string with a suffix.
*/
export const suffixFormatter = (unit = '', valueFactor = 1) => {
return (value, fractionDigits, maxLength) => {
const length = maxLength !== undefined ? maxLength - unit.length : undefined;
return `${formatNumber(value, { fractionDigits, maxLength: length, valueFactor })}${unit}`;
};
};
/**
* Returns a function that formats a number scaled using SI units notation.
*/
export const scaledSIFormatter = (unit = '', prefixOffset = 0) => {
const fractional = ['y', 'z', 'a', 'f', 'p', 'n', 'µ', 'm'];
const multiplicative = ['k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];
const symbols = [...fractional, '', ...multiplicative];
const units = symbols.slice(fractional.length + prefixOffset).map(prefix => {
return `${prefix}${unit}`;
});
if (!units.length) {
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
throw new RangeError('The unit cannot be converted, please try a different scale');
}
return scaledFormatter(units);
};
import { s__ } from '~/locale';
import { suffixFormatter, scaledSIFormatter, numberFormatter } from './formatter_factory';
/**
* Supported formats
*/
export const SUPPORTED_FORMATS = {
// Number
number: 'number',
percent: 'percent',
percentHundred: 'percentHundred',
// Duration
seconds: 'seconds',
miliseconds: 'miliseconds',
// Digital
bytes: 'bytes',
kilobytes: 'kilobytes',
megabytes: 'megabytes',
gigabytes: 'gigabytes',
terabytes: 'terabytes',
petabytes: 'petabytes',
};
/**
* Returns a function that formats number to different units
* @param {String} format - Format to use, must be one of the SUPPORTED_FORMATS. Defaults to number.
*
*
*/
export const getFormatter = (format = SUPPORTED_FORMATS.number) => {
// Number
if (format === SUPPORTED_FORMATS.number) {
/**
* Formats a number
*
* @function
* @param {Number} value - Number to format
* @param {Number} fractionDigits - precision decimals
* @param {Number} maxLength - Max lenght of formatted number
* if lenght is exceeded, exponential format is used.
*/
return numberFormatter();
}
if (format === SUPPORTED_FORMATS.percent) {
/**
* Formats a percentge (0 - 1)
*
* @function
* @param {Number} value - Number to format, `1` is rendered as `100%`
* @param {Number} fractionDigits - number of precision decimals
* @param {Number} maxLength - Max lenght of formatted number
* if lenght is exceeded, exponential format is used.
*/
return numberFormatter('percent');
}
if (format === SUPPORTED_FORMATS.percentHundred) {
/**
* Formats a percentge (0 to 100)
*
* @function
* @param {Number} value - Number to format, `100` is rendered as `100%`
* @param {Number} fractionDigits - number of precision decimals
* @param {Number} maxLength - Max lenght of formatted number
* if lenght is exceeded, exponential format is used.
*/
return numberFormatter('percent', 1 / 100);
}
// Durations
if (format === SUPPORTED_FORMATS.seconds) {
/**
* Formats a number of seconds
*
* @function
* @param {Number} value - Number to format, `1` is rendered as `1s`
* @param {Number} fractionDigits - number of precision decimals
* @param {Number} maxLength - Max lenght of formatted number
* if lenght is exceeded, exponential format is used.
*/
return suffixFormatter(s__('Units|s'));
}
if (format === SUPPORTED_FORMATS.miliseconds) {
/**
* Formats a number of miliseconds with ms as units
*
* @function
* @param {Number} value - Number to format, `1` is formatted as `1ms`
* @param {Number} fractionDigits - number of precision decimals
* @param {Number} maxLength - Max lenght of formatted number
* if lenght is exceeded, exponential format is used.
*/
return suffixFormatter(s__('Units|ms'));
}
// Digital
if (format === SUPPORTED_FORMATS.bytes) {
/**
* Formats a number of bytes scaled up to larger digital
* units for larger numbers.
*
* @function
* @param {Number} value - Number to format, `1` is formatted as `1B`
* @param {Number} fractionDigits - number of precision decimals
*/
return scaledSIFormatter('B');
}
if (format === SUPPORTED_FORMATS.kilobytes) {
/**
* Formats a number of kilobytes scaled up to larger digital
* units for larger numbers.
*
* @function
* @param {Number} value - Number to format, `1` is formatted as `1kB`
* @param {Number} fractionDigits - number of precision decimals
*/
return scaledSIFormatter('B', 1);
}
if (format === SUPPORTED_FORMATS.megabytes) {
/**
* Formats a number of megabytes scaled up to larger digital
* units for larger numbers.
*
* @function
* @param {Number} value - Number to format, `1` is formatted as `1MB`
* @param {Number} fractionDigits - number of precision decimals
*/
return scaledSIFormatter('B', 2);
}
if (format === SUPPORTED_FORMATS.gigabytes) {
/**
* Formats a number of gigabytes scaled up to larger digital
* units for larger numbers.
*
* @function
* @param {Number} value - Number to format, `1` is formatted as `1GB`
* @param {Number} fractionDigits - number of precision decimals
*/
return scaledSIFormatter('B', 3);
}
if (format === SUPPORTED_FORMATS.terabytes) {
/**
* Formats a number of terabytes scaled up to larger digital
* units for larger numbers.
*
* @function
* @param {Number} value - Number to format, `1` is formatted as `1GB`
* @param {Number} fractionDigits - number of precision decimals
*/
return scaledSIFormatter('B', 4);
}
if (format === SUPPORTED_FORMATS.petabytes) {
/**
* Formats a number of petabytes scaled up to larger digital
* units for larger numbers.
*
* @function
* @param {Number} value - Number to format, `1` is formatted as `1PB`
* @param {Number} fractionDigits - number of precision decimals
*/
return scaledSIFormatter('B', 5);
}
// Fail so client library addresses issue
throw TypeError(`${format} is not a valid number format`);
};
...@@ -4,7 +4,7 @@ import { GlLink, GlButton, GlTooltip, GlResizeObserverDirective } from '@gitlab/ ...@@ -4,7 +4,7 @@ import { GlLink, GlButton, GlTooltip, GlResizeObserverDirective } from '@gitlab/
import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts'; import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
import dateFormat from 'dateformat'; import dateFormat from 'dateformat';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import { roundOffFloat } from '~/lib/utils/common_utils'; import { getFormatter } from '~/lib/utils/unit_format';
import { getSvgIconPathContent } from '~/lib/utils/icon_utils'; import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { import {
...@@ -37,6 +37,8 @@ const events = { ...@@ -37,6 +37,8 @@ const events = {
datazoom: 'datazoom', datazoom: 'datazoom',
}; };
const yValFormatter = getFormatter('number');
export default { export default {
components: { components: {
GlAreaChart, GlAreaChart,
...@@ -171,7 +173,7 @@ export default { ...@@ -171,7 +173,7 @@ export default {
boundaryGap: [0.1, 0.1], boundaryGap: [0.1, 0.1],
scale: true, scale: true,
axisLabel: { axisLabel: {
formatter: num => roundOffFloat(num, 3).toString(), formatter: num => yValFormatter(num, 3),
}, },
...yAxis, ...yAxis,
}; };
...@@ -313,7 +315,8 @@ export default { ...@@ -313,7 +315,8 @@ export default {
this.tooltip.commitUrl = deploy.commitUrl; this.tooltip.commitUrl = deploy.commitUrl;
} else { } else {
const { seriesName, color, dataIndex } = dataPoint; const { seriesName, color, dataIndex } = dataPoint;
const value = yVal.toFixed(3); const value = yValFormatter(yVal, 3);
this.tooltip.content.push({ this.tooltip.content.push({
name: seriesName, name: seriesName,
dataIndex, dataIndex,
......
<script> <script>
import { GlLoadingIcon } from '@gitlab/ui';
export default { export default {
name: 'ResolveDiscussionButton', name: 'ResolveDiscussionButton',
components: {
GlLoadingIcon,
},
props: { props: {
isResolving: { isResolving: {
type: Boolean, type: Boolean,
...@@ -17,12 +22,7 @@ export default { ...@@ -17,12 +22,7 @@ export default {
<template> <template>
<button ref="button" type="button" class="btn btn-default ml-sm-2" @click="$emit('onClick')"> <button ref="button" type="button" class="btn btn-default ml-sm-2" @click="$emit('onClick')">
<i <gl-loading-icon v-if="isResolving" ref="isResolvingIcon" inline />
v-if="isResolving"
ref="isResolvingIcon"
aria-hidden="true"
class="fa fa-spinner fa-spin"
></i>
{{ buttonTitle }} {{ buttonTitle }}
</button> </button>
</template> </template>
...@@ -13,16 +13,14 @@ ...@@ -13,16 +13,14 @@
.page-title, .page-title,
.modal-title { .modal-title {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
.modal-title-with-label span { .modal-title-with-label span {
vertical-align: middle; vertical-align: middle;
display: inline-block; display: inline-block;
} }
.color-label {
font-size: $gl-font-size;
padding: $gl-vert-padding $label-padding-modal;
vertical-align: middle;
}
} }
.modal-title { .modal-title {
......
...@@ -51,7 +51,8 @@ ...@@ -51,7 +51,8 @@
} }
.btn { .btn {
.spinner { .spinner,
.gl-spinner {
vertical-align: text-bottom; vertical-align: text-bottom;
} }
} }
...@@ -86,14 +86,19 @@ ...@@ -86,14 +86,19 @@
} }
.issuable-show-labels { .issuable-show-labels {
a { .gl-label {
margin-bottom: 5px; margin-bottom: 5px;
margin-right: 5px; margin-right: 5px;
}
a {
display: inline-block; display: inline-block;
.color-label { .color-label {
padding: 4px $grid-size; padding: 4px $grid-size;
border-radius: $label-border-radius; border-radius: $label-border-radius;
margin-right: 4px;
margin-bottom: 4px;
} }
&:hover .color-label { &:hover .color-label {
...@@ -159,9 +164,25 @@ ...@@ -159,9 +164,25 @@
.avatar { .avatar {
border-color: rgba($gray-normal, 0.2); border-color: rgba($gray-normal, 0.2);
} }
}
}
a.gl-label-icon {
color: $gray-500;
}
.gl-label .gl-label-link:hover {
text-decoration: none;
color: inherit;
.gl-label-text:last-of-type {
text-decoration: underline;
} }
}
.gl-label .gl-label-icon:hover {
text-decoration: none;
color: $gray-500;
} }
.btn-link { .btn-link {
...@@ -800,11 +821,23 @@ ...@@ -800,11 +821,23 @@
a { a {
color: $gl-text-color; color: $gl-text-color;
}
.fa { .gl-label-link {
color: $gl-text-color-secondary; color: inherit;
&:hover {
text-decoration: none;
.gl-label-text:last-of-type {
text-decoration: underline;
}
} }
} }
.gl-label-icon {
color: $gray-500;
}
} }
@media(max-width: map-get($grid-breakpoints, lg)-1) { @media(max-width: map-get($grid-breakpoints, lg)-1) {
......
...@@ -127,6 +127,11 @@ ...@@ -127,6 +127,11 @@
.color-label { .color-label {
padding: $gl-padding-4 $grid-size; padding: $gl-padding-4 $grid-size;
} }
.prepend-description-left {
vertical-align: top;
line-height: 24px;
}
} }
.prioritized-labels { .prioritized-labels {
...@@ -305,10 +310,13 @@ ...@@ -305,10 +310,13 @@
width: 150px; width: 150px;
flex-shrink: 0; flex-shrink: 0;
.badge { .scoped-label-wrapper,
overflow: hidden; .gl-label {
text-overflow: ellipsis; line-height: $gl-line-height;
max-width: 100%; }
.gl-label-scoped .gl-label-text:last-of-type {
padding-right: 22px;
} }
} }
...@@ -445,10 +453,19 @@ ...@@ -445,10 +453,19 @@
} }
} }
.gl-label-scoped {
box-shadow: 0 0 0 2px currentColor inset;
&.gl-label-sm {
box-shadow: 0 0 0 1px inset;
}
}
// Label inside title of Delete Label Modal // Label inside title of Delete Label Modal
.modal-header .page-title { .modal-header .page-title {
.scoped-label-wrapper { .scoped-label-wrapper {
.scoped-label { .scoped-label,
.gl-label-icon {
line-height: 20px; line-height: 20px;
} }
......
...@@ -59,9 +59,19 @@ $status-box-line-height: 26px; ...@@ -59,9 +59,19 @@ $status-box-line-height: 26px;
} }
.issuable-row { .issuable-row {
span a { span {
color: $gl-text-color; a {
word-wrap: break-word; color: $gl-text-color;
word-wrap: break-word;
}
.gl-label-link {
color: inherit;
}
.gl-label-icon {
color: $gray-500;
}
} }
} }
......
...@@ -283,7 +283,7 @@ $note-form-margin-left: 72px; ...@@ -283,7 +283,7 @@ $note-form-margin-left: 72px;
text-transform: lowercase; text-transform: lowercase;
} }
a { a:not(.gl-link) {
color: $blue-600; color: $blue-600;
} }
...@@ -671,6 +671,16 @@ $note-form-margin-left: 72px; ...@@ -671,6 +671,16 @@ $note-form-margin-left: 72px;
a:hover { a:hover {
text-decoration: underline; text-decoration: underline;
} }
.gl-label-link:hover,
.gl-label-icon:hover {
text-decoration: none;
color: inherit;
.gl-label-text:last-of-type {
text-decoration: underline;
}
}
} }
/** /**
......
...@@ -36,37 +36,42 @@ module LabelsHelper ...@@ -36,37 +36,42 @@ module LabelsHelper
# link_to_label(label) { "My Custom Label Text" } # link_to_label(label) { "My Custom Label Text" }
# #
# Returns a String # Returns a String
def link_to_label(label, type: :issue, tooltip: true, css_class: nil, &block) def link_to_label(label, type: :issue, tooltip: true, small: false, &block)
link = label.filter_path(type: type) link = label.filter_path(type: type)
if block_given? if block_given?
link_to link, class: css_class, &block link_to link, &block
else else
render_label(label, tooltip: tooltip, link: link, css: css_class) render_label(label, link: link, tooltip: tooltip, small: small)
end end
end end
def render_label(label, tooltip: true, link: nil, css: nil, dataset: nil) def render_label(label, link: nil, tooltip: true, dataset: nil, small: false)
# if scoped label is used then EE wraps label tag with scoped label html = render_colored_label(label)
# doc link
html = render_colored_label(label, tooltip: tooltip)
html = link_to(html, link, class: css, data: dataset) if link
html if link
title = label_tooltip_title(label) if tooltip
html = render_label_link(html, link: link, title: title, dataset: dataset)
end
wrap_label_html(html, small: small, label: label)
end end
def render_colored_label(label, label_suffix: '', tooltip: true, title: nil) def render_colored_label(label, suffix: '')
text_color = text_color_for_bg(label.color) render_label_text(
title ||= tooltip ? label_tooltip_title(label) : label.name label.name,
suffix: suffix,
css_class: text_color_class_for_bg(label.color),
bg_color: label.color
)
end
# Intentionally not using content_tag here so that this method can be called # We need the `label` argument here for EE
# by LabelReferenceFilter def wrap_label_html(label_html, small:, label:)
span = %(<span class="badge color-label #{"has-tooltip" if tooltip}" ) + wrapper_classes = %w(gl-label)
%(data-html="true" style="background-color: #{label.color}; color: #{text_color}" ) + wrapper_classes << 'gl-label-sm' if small
%(title="#{ERB::Util.html_escape_once(title)}" data-container="body">) +
%(#{ERB::Util.html_escape_once(label.name)}#{label_suffix}</span>)
span.html_safe %(<span class="#{wrapper_classes.join(' ')}">#{label_html}</span>).html_safe
end end
def label_tooltip_title(label) def label_tooltip_title(label)
...@@ -109,6 +114,20 @@ module LabelsHelper ...@@ -109,6 +114,20 @@ module LabelsHelper
end end
end end
def text_color_class_for_bg(bg_color)
if bg_color.length == 4
r, g, b = bg_color[1, 4].scan(/./).map { |v| (v * 2).hex }
else
r, g, b = bg_color[1, 7].scan(/.{2}/).map(&:hex)
end
if (r + g + b) > 500
'gl-label-text-dark'
else
'gl-label-text-light'
end
end
def text_color_for_bg(bg_color) def text_color_for_bg(bg_color)
if bg_color.length == 4 if bg_color.length == 4
r, g, b = bg_color[1, 4].scan(/./).map { |v| (v * 2).hex } r, g, b = bg_color[1, 4].scan(/./).map { |v| (v * 2).hex }
...@@ -246,6 +265,31 @@ module LabelsHelper ...@@ -246,6 +265,31 @@ module LabelsHelper
def issuable_types def issuable_types
['issues', 'merge requests'] ['issues', 'merge requests']
end end
private
def render_label_link(label_html, link:, title:, dataset:)
classes = %w(gl-link gl-label-link)
dataset ||= {}
if title.present?
classes << 'has-tooltip'
dataset.merge!(html: true, title: title)
end
link_to(label_html, link, class: classes.join(' '), data: dataset)
end
def render_label_text(name, suffix: '', css_class: nil, bg_color: nil)
<<~HTML.chomp.html_safe
<span
class="gl-label-text #{css_class}"
data-container="body"
data-html="true"
#{"style=\"background-color: #{bg_color}\"" if bg_color}
>#{ERB::Util.html_escape_once(name)}#{suffix}</span>
HTML
end
end end
LabelsHelper.prepend_if_ee('EE::LabelsHelper') LabelsHelper.prepend_if_ee('EE::LabelsHelper')
...@@ -11,15 +11,15 @@ module Clusters ...@@ -11,15 +11,15 @@ module Clusters
self.table_name = 'clusters' self.table_name = 'clusters'
APPLICATIONS = { APPLICATIONS = {
Applications::Helm.application_name => Applications::Helm, Clusters::Applications::Helm.application_name => Clusters::Applications::Helm,
Applications::Ingress.application_name => Applications::Ingress, Clusters::Applications::Ingress.application_name => Clusters::Applications::Ingress,
Applications::CertManager.application_name => Applications::CertManager, Clusters::Applications::CertManager.application_name => Clusters::Applications::CertManager,
Applications::Crossplane.application_name => Applications::Crossplane, Clusters::Applications::Crossplane.application_name => Clusters::Applications::Crossplane,
Applications::Prometheus.application_name => Applications::Prometheus, Clusters::Applications::Prometheus.application_name => Clusters::Applications::Prometheus,
Applications::Runner.application_name => Applications::Runner, Clusters::Applications::Runner.application_name => Clusters::Applications::Runner,
Applications::Jupyter.application_name => Applications::Jupyter, Clusters::Applications::Jupyter.application_name => Clusters::Applications::Jupyter,
Applications::Knative.application_name => Applications::Knative, Clusters::Applications::Knative.application_name => Clusters::Applications::Knative,
Applications::ElasticStack.application_name => Applications::ElasticStack Clusters::Applications::ElasticStack.application_name => Clusters::Applications::ElasticStack
}.freeze }.freeze
DEFAULT_ENVIRONMENT = '*' DEFAULT_ENVIRONMENT = '*'
KUBE_INGRESS_BASE_DOMAIN = 'KUBE_INGRESS_BASE_DOMAIN' KUBE_INGRESS_BASE_DOMAIN = 'KUBE_INGRESS_BASE_DOMAIN'
......
...@@ -101,7 +101,7 @@ module Issuable ...@@ -101,7 +101,7 @@ module Issuable
def create_milestone_note def create_milestone_note
if milestone_changes_tracking_enabled? if milestone_changes_tracking_enabled?
# Creates a synthetic note # Creates a synthetic note
ResourceEvents::ChangeMilestoneService.new(resource: issuable, user: current_user).execute ResourceEvents::ChangeMilestoneService.new(issuable, current_user).execute
else else
SystemNoteService.change_milestone(issuable, issuable.project, current_user, issuable.milestone) SystemNoteService.change_milestone(issuable, issuable.project, current_user, issuable.milestone)
end end
......
...@@ -4,7 +4,7 @@ module ResourceEvents ...@@ -4,7 +4,7 @@ module ResourceEvents
class ChangeMilestoneService class ChangeMilestoneService
attr_reader :resource, :user, :event_created_at, :milestone attr_reader :resource, :user, :event_created_at, :milestone
def initialize(resource:, user:, created_at: Time.now) def initialize(resource, user, created_at: Time.now)
@resource = resource @resource = resource
@user = user @user = user
@event_created_at = created_at @event_created_at = created_at
......
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
- if issue.labels.any? - if issue.labels.any?
&nbsp; &nbsp;
- presented_labels_sorted_by_title(issue.labels, issue.project).each do |label| - presented_labels_sorted_by_title(issue.labels, issue.project).each do |label|
= link_to_label(label, css_class: 'label-link') = link_to_label(label, small: true)
= render_if_exists "projects/issues/issue_weight", issue: issue = render_if_exists "projects/issues/issue_weight", issue: issue
......
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
- if merge_request.labels.any? - if merge_request.labels.any?
&nbsp; &nbsp;
- presented_labels_sorted_by_title(merge_request.labels, merge_request.project).each do |label| - presented_labels_sorted_by_title(merge_request.labels, merge_request.project).each do |label|
= link_to_label(label, type: :merge_request, css_class: 'label-link') = link_to_label(label, type: :merge_request, small: true)
.issuable-meta .issuable-meta
%ul.controls.d-flex.align-items-end %ul.controls.d-flex.align-items-end
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
.modal-dialog .modal-dialog
.modal-content .modal-content
.modal-header .modal-header
%h3.page-title Delete #{render_label(label, tooltip: false)} ? %h3.page-title Delete label: #{label.name} ?
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') } %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": true } &times; %span{ "aria-hidden": true } &times;
......
...@@ -29,11 +29,14 @@ ...@@ -29,11 +29,14 @@
":title" => '(list.assignee && list.assignee.username || "")' } ":title" => '(list.assignee && list.assignee.username || "")' }
@{{ list.assignee.username }} @{{ list.assignee.username }}
%span.has-tooltip.badge.color-label.title.d-inline-block.mw-100.text-truncate.align-middle{ "v-if": "list.type === \"label\"", %gl-label{ "v-if" => " list.type === \"label\"",
":title" => '(list.label ? list.label.description : "")', ":background-color" => "list.label.color",
data: { container: "body", placement: "bottom" }, ":title" => "list.label.title",
":style" => "{ backgroundColor: (list.label && list.label.color ? list.label.color : null), color: (list.label && list.label.textColor ? list.label.textColor : \"#2e2e2e\") }" } ":description" => "list.label.description",
{{ list.title }} "tooltipPlacement" => "bottom",
":size" => '(!list.isExpanded ? "sm" : "")',
":scoped" => "showScopedLabels(list.label)",
":scoped-labels-documentation-link" => "helpLink" }
- if can?(current_user, :admin_list, current_board_parent) - if can?(current_user, :admin_list, current_board_parent)
%board-delete{ "inline-template" => true, %board-delete{ "inline-template" => true,
......
...@@ -8,15 +8,12 @@ ...@@ -8,15 +8,12 @@
%span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" } %span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" }
= _("None") = _("None")
%span{ "v-for" => "label in issue.labels" } %span{ "v-for" => "label in issue.labels" }
%span.d-inline-block.position-relative.scoped-label-wrapper{ "v-if" => "showScopedLabels(label)" } %gl-label{ ":key" => "label.id",
%a{ href: '#' } ":background-color" => "label.color",
%span.badge.color-label.label{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" } ":title" => "label.title",
{{ label.title }} ":description" => "label.description",
%a.label.scoped-label{ ":href" => "helpLink()" } ":scoped" => "showScopedLabels(label)",
%i.fa.fa-question-circle{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" } ":scoped-labels-documentation-link" => "helpLink" }
%a{ href: "#", "v-else" => true }
.badge.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" }
{{ label.title }}
- if can_admin_issue? - if can_admin_issue?
.selectbox .selectbox
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
%span.issuable-number= issuable.to_reference %span.issuable-number= issuable.to_reference
- labels.each do |label| - labels.each do |label|
= render_label(label.present(issuable_subject: project), link: polymorphic_path(issuable_type_args, { milestone_title: @milestone.title, label_name: label.title, state: 'all' })) = render_label(label.present(issuable_subject: project), link: polymorphic_path(issuable_type_args, { milestone_title: @milestone.title, label_name: label.title, state: 'all' }), small: true)
%span.assignee-icon %span.assignee-icon
- assignees.each do |assignee| - assignees.each do |assignee|
......
...@@ -3,11 +3,9 @@ ...@@ -3,11 +3,9 @@
- options = { milestone_title: @milestone.title, label_name: label.title } - options = { milestone_title: @milestone.title, label_name: label.title }
%li.no-border %li.no-border
%span.label-row = render_label(label, tooltip: false, link: milestones_label_path(options))
%span.label-name %span.prepend-description-left
= render_label(label, tooltip: false, link: milestones_label_path(options)) = markdown_field(label, :description)
%span.prepend-description-left
= markdown_field(label, :description)
.float-right.d-none.d-lg-block.d-xl-block .float-right.d-none.d-lg-block.d-xl-block
= link_to milestones_label_path(options.merge(state: 'opened')), class: 'btn btn-transparent btn-action' do = link_to milestones_label_path(options.merge(state: 'opened')), class: 'btn btn-transparent btn-action' do
......
---
title: Replaced underscore with lodash for app/assets/javascripts/lib
merge_request: 25042
author: Shubham Pandey
type: other
---
title: New styles for scoped labels
merge_request: 21377
author:
type: changed
---
title: Migrate .fa-spinner to .spinner for app/assets/javascripts/notes/components/discussion_resolve_button.vue
merge_request: 25055
author: nuwe1
type: other
...@@ -93,23 +93,26 @@ module Banzai ...@@ -93,23 +93,26 @@ module Banzai
end end
presenter = object.present(issuable_subject: parent) presenter = object.present(issuable_subject: parent)
LabelsHelper.render_colored_label(presenter, label_suffix: label_suffix, title: tooltip_title(presenter)) LabelsHelper.render_colored_label(presenter, suffix: label_suffix)
end end
def tooltip_title(label) def wrap_link(link, label)
nil presenter = label.present(issuable_subject: project || group)
LabelsHelper.wrap_label_html(link, small: true, label: presenter)
end end
def full_path_ref?(matches) def full_path_ref?(matches)
matches[:namespace] && matches[:project] matches[:namespace] && matches[:project]
end end
def reference_class(type, tooltip: true)
super + ' gl-link gl-label-link'
end
def object_link_title(object, matches) def object_link_title(object, matches)
# use title of wrapped element instead presenter = object.present(issuable_subject: project || group)
nil LabelsHelper.label_tooltip_title(presenter)
end end
end end
end end
end end
Banzai::Filter::LabelReferenceFilter.prepend_if_ee('EE::Banzai::Filter::LabelReferenceFilter')
...@@ -37,7 +37,8 @@ module Banzai ...@@ -37,7 +37,8 @@ module Banzai
attributes[:reference_type] ||= self.class.reference_type attributes[:reference_type] ||= self.class.reference_type
attributes[:container] ||= 'body' attributes[:container] ||= 'body'
attributes[:placement] ||= 'bottom' attributes[:placement] ||= 'top'
attributes[:html] ||= 'true'
attributes.delete(:original) if context[:no_original_data] attributes.delete(:original) if context[:no_original_data]
attributes.map do |key, value| attributes.map do |key, value|
%Q(data-#{key.to_s.dasherize}="#{escape_once(value)}") %Q(data-#{key.to_s.dasherize}="#{escape_once(value)}")
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
module Gitlab module Gitlab
module MarkdownCache module MarkdownCache
# Increment this number every time the renderer changes its output # Increment this number every time the renderer changes its output
CACHE_COMMONMARK_VERSION = 18 CACHE_COMMONMARK_VERSION = 19
CACHE_COMMONMARK_VERSION_START = 10 CACHE_COMMONMARK_VERSION_START = 10
BaseError = Class.new(StandardError) BaseError = Class.new(StandardError)
......
...@@ -5848,6 +5848,9 @@ msgstr "" ...@@ -5848,6 +5848,9 @@ msgstr ""
msgid "CustomCycleAnalytics|Select stop event" msgid "CustomCycleAnalytics|Select stop event"
msgstr "" msgstr ""
msgid "CustomCycleAnalytics|Stage name already exists"
msgstr ""
msgid "CustomCycleAnalytics|Start event" msgid "CustomCycleAnalytics|Start event"
msgstr "" msgstr ""
...@@ -18564,9 +18567,6 @@ msgstr "" ...@@ -18564,9 +18567,6 @@ msgstr ""
msgid "Subscription deletion failed." msgid "Subscription deletion failed."
msgstr "" msgstr ""
msgid "Subscription successfully applied to \"%{group_name}\""
msgstr ""
msgid "Subscription successfully created." msgid "Subscription successfully created."
msgstr "" msgstr ""
...@@ -20756,6 +20756,12 @@ msgstr "" ...@@ -20756,6 +20756,12 @@ msgstr ""
msgid "Uninstalling" msgid "Uninstalling"
msgstr "" msgstr ""
msgid "Units|ms"
msgstr ""
msgid "Units|s"
msgstr ""
msgid "Unknown" msgid "Unknown"
msgstr "" msgstr ""
......
...@@ -277,7 +277,7 @@ describe 'Issue Boards', :js do ...@@ -277,7 +277,7 @@ describe 'Issue Boards', :js do
wait_for_requests wait_for_requests
page.within('.value') do page.within('.value') do
expect(page).to have_selector('.badge', count: 2) expect(page).to have_selector('.gl-label-text', count: 2)
expect(page).to have_content(development.title) expect(page).to have_content(development.title)
expect(page).to have_content(stretch.title) expect(page).to have_content(stretch.title)
end end
...@@ -299,7 +299,7 @@ describe 'Issue Boards', :js do ...@@ -299,7 +299,7 @@ describe 'Issue Boards', :js do
find('.dropdown-menu-close-icon').click find('.dropdown-menu-close-icon').click
page.within('.value') do page.within('.value') do
expect(page).to have_selector('.badge', count: 3) expect(page).to have_selector('.gl-label-text', count: 3)
expect(page).to have_content(bug.title) expect(page).to have_content(bug.title)
end end
end end
...@@ -328,7 +328,7 @@ describe 'Issue Boards', :js do ...@@ -328,7 +328,7 @@ describe 'Issue Boards', :js do
find('.dropdown-menu-close-icon').click find('.dropdown-menu-close-icon').click
page.within('.value') do page.within('.value') do
expect(page).to have_selector('.badge', count: 4) expect(page).to have_selector('.gl-label-text', count: 4)
expect(page).to have_content(bug.title) expect(page).to have_content(bug.title)
expect(page).to have_content(regression.title) expect(page).to have_content(regression.title)
end end
...@@ -357,13 +357,13 @@ describe 'Issue Boards', :js do ...@@ -357,13 +357,13 @@ describe 'Issue Boards', :js do
find('.dropdown-menu-close-icon').click find('.dropdown-menu-close-icon').click
page.within('.value') do page.within('.value') do
expect(page).to have_selector('.badge', count: 1) expect(page).to have_selector('.gl-label-text', count: 1)
expect(page).not_to have_content(stretch.title) expect(page).not_to have_content(stretch.title)
end end
end end
# 'Development' label does not show since the card is in a 'Development' list label # 'Development' label does not show since the card is in a 'Development' list label
expect(card).to have_selector('.badge', count: 0) expect(card).to have_selector('.gl-label-text', count: 0)
expect(card).not_to have_content(stretch.title) expect(card).not_to have_content(stretch.title)
end end
......
...@@ -4,30 +4,31 @@ require 'spec_helper' ...@@ -4,30 +4,31 @@ require 'spec_helper'
describe 'Container Registry', :js do describe 'Container Registry', :js do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project) } let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
let(:container_repository) do let(:container_repository) do
create(:container_repository, name: 'my/image') create(:container_repository, name: 'my/image')
end end
before do before do
group.add_owner(user)
sign_in(user) sign_in(user)
project.add_developer(user)
stub_container_registry_config(enabled: true) stub_container_registry_config(enabled: true)
stub_container_registry_tags(repository: :any, tags: []) stub_container_registry_tags(repository: :any, tags: [])
stub_feature_flags(vue_container_registry_explorer: false)
end end
it 'has a page title set' do it 'has a page title set' do
visit_container_registry visit_container_registry
expect(page).to have_title(_('Container Registry'))
expect(page).to have_title _('Container Registry')
end end
context 'when there are no image repositories' do context 'when there are no image repositories' do
it 'user visits container registry main page' do it 'list page has no container title' do
visit_container_registry visit_container_registry
expect(page).to have_content 'no container images' expect(page).to have_content _('There are no container images available in this group')
end end
end end
...@@ -37,39 +38,56 @@ describe 'Container Registry', :js do ...@@ -37,39 +38,56 @@ describe 'Container Registry', :js do
project.container_repositories << container_repository project.container_repositories << container_repository
end end
it 'user wants to see multi-level container repository' do it 'list page has a list of images' do
visit_container_registry visit_container_registry
expect(page).to have_content('my/image') expect(page).to have_content 'my/image'
end end
it 'user removes entire container repository', :sidekiq_might_not_need_inline do it 'image repository delete is disabled' do
visit_container_registry visit_container_registry
expect_any_instance_of(ContainerRepository).to receive(:delete_tags!).and_return(true) delete_btn = find('[title="Remove repository"]')
expect(delete_btn).to be_disabled
end
it 'navigates to repo details' do
visit_container_registry_details('my/image')
click_on(class: 'js-remove-repo') expect(page).to have_content 'latest'
expect(find('.modal .modal-title')).to have_content 'Remove repository'
find('.modal .modal-footer .btn-danger').click
end end
it 'user removes a specific tag from container repository' do describe 'image repo details' do
visit_container_registry before do
visit_container_registry_details 'my/image'
end
find('.js-toggle-repo').click it 'shows the details breadcrumb' do
wait_for_requests expect(find('.breadcrumbs')).to have_link 'my/image'
end
service = double('service') it 'shows the image title' do
expect(service).to receive(:execute).with(container_repository) { { status: :success } } expect(page).to have_content 'my/image tags'
expect(Projects::ContainerRepository::DeleteTagsService).to receive(:new).with(container_repository.project, user, tags: ['latest']) { service } end
click_on(class: 'js-delete-registry-row', visible: false) it 'user removes a specific tag from container repository' do
expect(find('.modal .modal-title')).to have_content 'Remove tag' service = double('service')
find('.modal .modal-footer .btn-danger').click expect(service).to receive(:execute).with(container_repository) { { status: :success } }
expect(Projects::ContainerRepository::DeleteTagsService).to receive(:new).with(container_repository.project, user, tags: ['latest']) { service }
click_on(class: 'js-delete-registry')
expect(find('.modal .modal-title')).to have_content _('Remove tag')
find('.modal .modal-footer .btn-danger').click
end
end end
end end
def visit_container_registry def visit_container_registry
visit project_container_registry_index_path(project) visit group_container_registries_path(group)
end
def visit_container_registry_details(name)
visit_container_registry
click_link(name)
end end
end end
...@@ -41,10 +41,10 @@ describe 'issuable list' do ...@@ -41,10 +41,10 @@ describe 'issuable list' do
visit_issuable_list(issuable_type) visit_issuable_list(issuable_type)
expect(all('.label-link')[0].text).to have_content('B') expect(all('.gl-label-text')[0].text).to have_content('B')
expect(all('.label-link')[1].text).to have_content('X') expect(all('.gl-label-text')[1].text).to have_content('X')
expect(all('.label-link')[2].text).to have_content('a') expect(all('.gl-label-text')[2].text).to have_content('a')
expect(all('.label-link')[3].text).to have_content('z') expect(all('.gl-label-text')[3].text).to have_content('z')
end end
end end
......
...@@ -332,7 +332,7 @@ describe 'Filter issues', :js do ...@@ -332,7 +332,7 @@ describe 'Filter issues', :js do
context 'issue label clicked' do context 'issue label clicked' do
it 'filters and displays in search bar' do it 'filters and displays in search bar' do
find('.issues-list .issue .issuable-main-info .issuable-info a .badge', text: multiple_words_label.title).click find('.issues-list .issue .issuable-main-info .issuable-info a .gl-label-text', text: multiple_words_label.title).click
expect_issues_list_count(1) expect_issues_list_count(1)
expect_tokens([label_token("\"#{multiple_words_label.title}\"")]) expect_tokens([label_token("\"#{multiple_words_label.title}\"")])
......
...@@ -161,9 +161,9 @@ describe 'Labels Hierarchy', :js do ...@@ -161,9 +161,9 @@ describe 'Labels Hierarchy', :js do
find('.btn-success').click find('.btn-success').click
expect(page.find('.issue-details h2.title')).to have_content('new created issue') expect(page.find('.issue-details h2.title')).to have_content('new created issue')
expect(page).to have_selector('span.badge', text: grandparent_group_label.title) expect(page).to have_selector('span.gl-label-text', text: grandparent_group_label.title)
expect(page).to have_selector('span.badge', text: parent_group_label.title) expect(page).to have_selector('span.gl-label-text', text: parent_group_label.title)
expect(page).to have_selector('span.badge', text: project_label_1.title) expect(page).to have_selector('span.gl-label-text', text: project_label_1.title)
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
describe 'Container Registry', :js do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:container_repository) do
create(:container_repository, name: 'my/image')
end
before do
sign_in(user)
project.add_developer(user)
stub_container_registry_config(enabled: true)
stub_container_registry_tags(repository: :any, tags: [])
end
describe 'Registry explorer is off' do
before do
stub_feature_flags(vue_container_registry_explorer: false)
end
it 'has a page title set' do
visit_container_registry
expect(page).to have_title _('Container Registry')
end
context 'when there are no image repositories' do
it 'user visits container registry main page' do
visit_container_registry
expect(page).to have_content _('no container images')
end
end
context 'when there are image repositories' do
before do
stub_container_registry_tags(repository: %r{my/image}, tags: %w[latest], with_manifest: true)
project.container_repositories << container_repository
end
it 'user wants to see multi-level container repository' do
visit_container_registry
expect(page).to have_content 'my/image'
end
it 'user removes entire container repository', :sidekiq_might_not_need_inline do
visit_container_registry
expect_any_instance_of(ContainerRepository).to receive(:delete_tags!).and_return(true)
click_on(class: 'js-remove-repo')
expect(find('.modal .modal-title')).to have_content _('Remove repository')
find('.modal .modal-footer .btn-danger').click
end
it 'user removes a specific tag from container repository' do
visit_container_registry
find('.js-toggle-repo').click
wait_for_requests
service = double('service')
expect(service).to receive(:execute).with(container_repository) { { status: :success } }
expect(Projects::ContainerRepository::DeleteTagsService).to receive(:new).with(container_repository.project, user, tags: ['latest']) { service }
click_on(class: 'js-delete-registry-row', visible: false)
expect(find('.modal .modal-title')).to have_content _('Remove tag')
find('.modal .modal-footer .btn-danger').click
end
end
end
describe 'Registry explorer is on' do
it 'has a page title set' do
visit_container_registry
expect(page).to have_title _('Container Registry')
end
context 'when there are no image repositories' do
it 'list page has no container title' do
visit_container_registry
expect(page).to have_content _('There are no container images stored for this project')
end
it 'list page has quickstart' do
visit_container_registry
expect(page).to have_content _('Quick Start')
end
end
context 'when there are image repositories' do
before do
stub_container_registry_tags(repository: %r{my/image}, tags: %w[latest], with_manifest: true)
project.container_repositories << container_repository
end
it 'list page has a list of images' do
visit_container_registry
expect(page).to have_content 'my/image'
end
it 'user removes entire container repository', :sidekiq_might_not_need_inline do
visit_container_registry
expect_any_instance_of(ContainerRepository).to receive(:delete_tags!).and_return(true)
find('[title="Remove repository"]').click
expect(find('.modal .modal-title')).to have_content _('Remove repository')
find('.modal .modal-footer .btn-danger').click
end
it 'navigates to repo details' do
visit_container_registry_details('my/image')
expect(page).to have_content 'latest'
end
describe 'image repo details' do
before do
visit_container_registry_details 'my/image'
end
it 'shows the details breadcrumb' do
expect(find('.breadcrumbs')).to have_link 'my/image'
end
it 'shows the image title' do
expect(page).to have_content 'my/image tags'
end
it 'user removes a specific tag from container repository' do
service = double('service')
expect(service).to receive(:execute).with(container_repository) { { status: :success } }
expect(Projects::ContainerRepository::DeleteTagsService).to receive(:new).with(container_repository.project, user, tags: ['latest']) { service }
click_on(class: 'js-delete-registry')
expect(find('.modal .modal-title')).to have_content _('Remove tag')
find('.modal .modal-footer .btn-danger').click
end
end
end
end
def visit_container_registry
visit project_container_registry_index_path(project)
end
def visit_container_registry_details(name)
visit_container_registry
click_link(name)
end
end
import {
numberFormatter,
suffixFormatter,
scaledSIFormatter,
} from '~/lib/utils/unit_format/formatter_factory';
describe('unit_format/formatter_factory', () => {
describe('numberFormatter', () => {
let formatNumber;
beforeEach(() => {
formatNumber = numberFormatter();
});
it('formats a integer', () => {
expect(formatNumber(1)).toEqual('1');
expect(formatNumber(100)).toEqual('100');
expect(formatNumber(1000)).toEqual('1,000');
expect(formatNumber(10000)).toEqual('10,000');
expect(formatNumber(1000000)).toEqual('1,000,000');
});
it('formats a floating point number', () => {
expect(formatNumber(0.1)).toEqual('0.1');
expect(formatNumber(0.1, 0)).toEqual('0');
expect(formatNumber(0.1, 2)).toEqual('0.10');
expect(formatNumber(0.1, 3)).toEqual('0.100');
expect(formatNumber(12.345)).toEqual('12.345');
expect(formatNumber(12.345, 2)).toEqual('12.35');
expect(formatNumber(12.345, 4)).toEqual('12.3450');
});
it('formats a large integer with a length limit', () => {
expect(formatNumber(10 ** 7, undefined)).toEqual('10,000,000');
expect(formatNumber(10 ** 7, undefined, 9)).toEqual('1.00e+7');
expect(formatNumber(10 ** 7, undefined, 10)).toEqual('10,000,000');
});
});
describe('suffixFormatter', () => {
let formatSuffix;
beforeEach(() => {
formatSuffix = suffixFormatter('pop.', undefined);
});
it('formats a integer', () => {
expect(formatSuffix(1)).toEqual('1pop.');
expect(formatSuffix(100)).toEqual('100pop.');
expect(formatSuffix(1000)).toEqual('1,000pop.');
expect(formatSuffix(10000)).toEqual('10,000pop.');
expect(formatSuffix(1000000)).toEqual('1,000,000pop.');
});
it('formats a floating point number', () => {
expect(formatSuffix(0.1)).toEqual('0.1pop.');
expect(formatSuffix(0.1, 0)).toEqual('0pop.');
expect(formatSuffix(0.1, 2)).toEqual('0.10pop.');
expect(formatSuffix(0.1, 3)).toEqual('0.100pop.');
expect(formatSuffix(12.345)).toEqual('12.345pop.');
expect(formatSuffix(12.345, 2)).toEqual('12.35pop.');
expect(formatSuffix(12.345, 4)).toEqual('12.3450pop.');
});
it('formats a negative integer', () => {
expect(formatSuffix(-1)).toEqual('-1pop.');
expect(formatSuffix(-100)).toEqual('-100pop.');
expect(formatSuffix(-1000)).toEqual('-1,000pop.');
expect(formatSuffix(-10000)).toEqual('-10,000pop.');
expect(formatSuffix(-1000000)).toEqual('-1,000,000pop.');
});
it('formats a floating point nugative number', () => {
expect(formatSuffix(-0.1)).toEqual('-0.1pop.');
expect(formatSuffix(-0.1, 0)).toEqual('-0pop.');
expect(formatSuffix(-0.1, 2)).toEqual('-0.10pop.');
expect(formatSuffix(-0.1, 3)).toEqual('-0.100pop.');
expect(formatSuffix(-12.345)).toEqual('-12.345pop.');
expect(formatSuffix(-12.345, 2)).toEqual('-12.35pop.');
expect(formatSuffix(-12.345, 4)).toEqual('-12.3450pop.');
});
it('formats a large integer', () => {
expect(formatSuffix(10 ** 7)).toEqual('10,000,000pop.');
expect(formatSuffix(10 ** 10)).toEqual('10,000,000,000pop.');
});
it('formats a large integer with a length limit', () => {
expect(formatSuffix(10 ** 7, undefined, 10)).toEqual('1.00e+7pop.');
expect(formatSuffix(10 ** 10, undefined, 10)).toEqual('1.00e+10pop.');
});
});
describe('scaledSIFormatter', () => {
describe('scaled format', () => {
let formatScaled;
beforeEach(() => {
formatScaled = scaledSIFormatter('B');
});
it('formats bytes', () => {
expect(formatScaled(12.345)).toEqual('12.345B');
expect(formatScaled(12.345, 0)).toEqual('12B');
expect(formatScaled(12.345, 1)).toEqual('12.3B');
expect(formatScaled(12.345, 2)).toEqual('12.35B');
});
it('formats bytes in a scale', () => {
expect(formatScaled(1)).toEqual('1B');
expect(formatScaled(10)).toEqual('10B');
expect(formatScaled(10 ** 2)).toEqual('100B');
expect(formatScaled(10 ** 3)).toEqual('1kB');
expect(formatScaled(10 ** 4)).toEqual('10kB');
expect(formatScaled(10 ** 5)).toEqual('100kB');
expect(formatScaled(10 ** 6)).toEqual('1MB');
expect(formatScaled(10 ** 7)).toEqual('10MB');
expect(formatScaled(10 ** 8)).toEqual('100MB');
expect(formatScaled(10 ** 9)).toEqual('1GB');
expect(formatScaled(10 ** 10)).toEqual('10GB');
expect(formatScaled(10 ** 11)).toEqual('100GB');
});
});
describe('scaled format with offset', () => {
let formatScaled;
beforeEach(() => {
// formats gigabytes
formatScaled = scaledSIFormatter('B', 3);
});
it('formats floating point numbers', () => {
expect(formatScaled(12.345)).toEqual('12.345GB');
expect(formatScaled(12.345, 0)).toEqual('12GB');
expect(formatScaled(12.345, 1)).toEqual('12.3GB');
expect(formatScaled(12.345, 2)).toEqual('12.35GB');
});
it('formats large numbers scaled', () => {
expect(formatScaled(1)).toEqual('1GB');
expect(formatScaled(1, 1)).toEqual('1.0GB');
expect(formatScaled(10)).toEqual('10GB');
expect(formatScaled(10 ** 2)).toEqual('100GB');
expect(formatScaled(10 ** 3)).toEqual('1TB');
expect(formatScaled(10 ** 4)).toEqual('10TB');
expect(formatScaled(10 ** 5)).toEqual('100TB');
expect(formatScaled(10 ** 6)).toEqual('1PB');
expect(formatScaled(10 ** 7)).toEqual('10PB');
expect(formatScaled(10 ** 8)).toEqual('100PB');
expect(formatScaled(10 ** 9)).toEqual('1EB');
});
it('formatting of too large numbers is not suported', () => {
// formatting YB is out of range
expect(() => scaledSIFormatter('B', 9)).toThrow();
});
});
describe('scaled format with negative offset', () => {
let formatScaled;
beforeEach(() => {
formatScaled = scaledSIFormatter('g', -1);
});
it('formats floating point numbers', () => {
expect(formatScaled(12.345)).toEqual('12.345mg');
expect(formatScaled(12.345, 0)).toEqual('12mg');
expect(formatScaled(12.345, 1)).toEqual('12.3mg');
expect(formatScaled(12.345, 2)).toEqual('12.35mg');
});
it('formats large numbers scaled', () => {
expect(formatScaled(1)).toEqual('1mg');
expect(formatScaled(1, 1)).toEqual('1.0mg');
expect(formatScaled(10)).toEqual('10mg');
expect(formatScaled(10 ** 2)).toEqual('100mg');
expect(formatScaled(10 ** 3)).toEqual('1g');
expect(formatScaled(10 ** 4)).toEqual('10g');
expect(formatScaled(10 ** 5)).toEqual('100g');
expect(formatScaled(10 ** 6)).toEqual('1kg');
expect(formatScaled(10 ** 7)).toEqual('10kg');
expect(formatScaled(10 ** 8)).toEqual('100kg');
});
it('formats negative numbers scaled', () => {
expect(formatScaled(-12.345)).toEqual('-12.345mg');
expect(formatScaled(-12.345, 0)).toEqual('-12mg');
expect(formatScaled(-12.345, 1)).toEqual('-12.3mg');
expect(formatScaled(-12.345, 2)).toEqual('-12.35mg');
expect(formatScaled(-10)).toEqual('-10mg');
expect(formatScaled(-100)).toEqual('-100mg');
expect(formatScaled(-(10 ** 4))).toEqual('-10g');
});
});
});
});
import { getFormatter, SUPPORTED_FORMATS } from '~/lib/utils/unit_format';
describe('unit_format', () => {
describe('when a supported format is provided, the returned function formats', () => {
it('numbers, by default', () => {
expect(getFormatter()(1)).toEqual('1');
});
it('numbers', () => {
const formatNumber = getFormatter(SUPPORTED_FORMATS.number);
expect(formatNumber(1)).toEqual('1');
expect(formatNumber(100)).toEqual('100');
expect(formatNumber(1000)).toEqual('1,000');
expect(formatNumber(10000)).toEqual('10,000');
expect(formatNumber(1000000)).toEqual('1,000,000');
});
it('percent', () => {
const formatPercent = getFormatter(SUPPORTED_FORMATS.percent);
expect(formatPercent(1)).toEqual('100%');
expect(formatPercent(1, 2)).toEqual('100.00%');
expect(formatPercent(0.1)).toEqual('10%');
expect(formatPercent(0.5)).toEqual('50%');
expect(formatPercent(0.888888)).toEqual('89%');
expect(formatPercent(0.888888, 2)).toEqual('88.89%');
expect(formatPercent(0.888888, 5)).toEqual('88.88880%');
expect(formatPercent(2)).toEqual('200%');
expect(formatPercent(10)).toEqual('1,000%');
});
it('percentunit', () => {
const formatPercentHundred = getFormatter(SUPPORTED_FORMATS.percentHundred);
expect(formatPercentHundred(1)).toEqual('1%');
expect(formatPercentHundred(1, 2)).toEqual('1.00%');
expect(formatPercentHundred(88.8888)).toEqual('89%');
expect(formatPercentHundred(88.8888, 2)).toEqual('88.89%');
expect(formatPercentHundred(88.8888, 5)).toEqual('88.88880%');
expect(formatPercentHundred(100)).toEqual('100%');
expect(formatPercentHundred(100, 2)).toEqual('100.00%');
expect(formatPercentHundred(200)).toEqual('200%');
expect(formatPercentHundred(1000)).toEqual('1,000%');
});
it('seconds', () => {
expect(getFormatter(SUPPORTED_FORMATS.seconds)(1)).toEqual('1s');
});
it('miliseconds', () => {
const formatMiliseconds = getFormatter(SUPPORTED_FORMATS.miliseconds);
expect(formatMiliseconds(1)).toEqual('1ms');
expect(formatMiliseconds(100)).toEqual('100ms');
expect(formatMiliseconds(1000)).toEqual('1,000ms');
expect(formatMiliseconds(10000)).toEqual('10,000ms');
expect(formatMiliseconds(1000000)).toEqual('1,000,000ms');
});
it('bytes', () => {
const formatBytes = getFormatter(SUPPORTED_FORMATS.bytes);
expect(formatBytes(1)).toEqual('1B');
expect(formatBytes(1, 1)).toEqual('1.0B');
expect(formatBytes(10)).toEqual('10B');
expect(formatBytes(10 ** 2)).toEqual('100B');
expect(formatBytes(10 ** 3)).toEqual('1kB');
expect(formatBytes(10 ** 4)).toEqual('10kB');
expect(formatBytes(10 ** 5)).toEqual('100kB');
expect(formatBytes(10 ** 6)).toEqual('1MB');
expect(formatBytes(10 ** 7)).toEqual('10MB');
expect(formatBytes(10 ** 8)).toEqual('100MB');
expect(formatBytes(10 ** 9)).toEqual('1GB');
expect(formatBytes(10 ** 10)).toEqual('10GB');
expect(formatBytes(10 ** 11)).toEqual('100GB');
});
it('kilobytes', () => {
expect(getFormatter(SUPPORTED_FORMATS.kilobytes)(1)).toEqual('1kB');
expect(getFormatter(SUPPORTED_FORMATS.kilobytes)(1, 1)).toEqual('1.0kB');
});
it('megabytes', () => {
expect(getFormatter(SUPPORTED_FORMATS.megabytes)(1)).toEqual('1MB');
expect(getFormatter(SUPPORTED_FORMATS.megabytes)(1, 1)).toEqual('1.0MB');
});
it('gigabytes', () => {
expect(getFormatter(SUPPORTED_FORMATS.gigabytes)(1)).toEqual('1GB');
expect(getFormatter(SUPPORTED_FORMATS.gigabytes)(1, 1)).toEqual('1.0GB');
});
it('terabytes', () => {
expect(getFormatter(SUPPORTED_FORMATS.terabytes)(1)).toEqual('1TB');
expect(getFormatter(SUPPORTED_FORMATS.terabytes)(1, 1)).toEqual('1.0TB');
});
it('petabytes', () => {
expect(getFormatter(SUPPORTED_FORMATS.petabytes)(1)).toEqual('1PB');
expect(getFormatter(SUPPORTED_FORMATS.petabytes)(1, 1)).toEqual('1.0PB');
});
});
describe('when get formatter format is incorrect', () => {
it('formatter fails', () => {
expect(() => getFormatter('not-supported')(1)).toThrow();
});
});
});
...@@ -56,7 +56,7 @@ describe LabelsHelper do ...@@ -56,7 +56,7 @@ describe LabelsHelper do
context 'without subject' do context 'without subject' do
it "uses the label's project" do it "uses the label's project" do
expect(link_to_label(label_presenter)).to match %r{<a href="/#{label.project.full_path}/issues\?label_name%5B%5D=#{label.name}">.*</a>} expect(link_to_label(label_presenter)).to match %r{<a.*href="/#{label.project.full_path}/issues\?label_name%5B%5D=#{label.name}".*>.*</a>}m
end end
end end
...@@ -65,7 +65,7 @@ describe LabelsHelper do ...@@ -65,7 +65,7 @@ describe LabelsHelper do
let(:subject) { build(:project, namespace: namespace, name: 'bar3') } let(:subject) { build(:project, namespace: namespace, name: 'bar3') }
it 'links to project issues page' do it 'links to project issues page' do
expect(link_to_label(label_presenter)).to match %r{<a href="/foo3/bar3/issues\?label_name%5B%5D=#{label.name}">.*</a>} expect(link_to_label(label_presenter)).to match %r{<a.*href="/foo3/bar3/issues\?label_name%5B%5D=#{label.name}".*>.*</a>}m
end end
end end
...@@ -73,7 +73,7 @@ describe LabelsHelper do ...@@ -73,7 +73,7 @@ describe LabelsHelper do
let(:subject) { build(:group, name: 'bar') } let(:subject) { build(:group, name: 'bar') }
it 'links to group issues page' do it 'links to group issues page' do
expect(link_to_label(label_presenter)).to match %r{<a href="/groups/bar/-/issues\?label_name%5B%5D=#{label.name}">.*</a>} expect(link_to_label(label_presenter)).to match %r{<a.*href="/groups/bar/-/issues\?label_name%5B%5D=#{label.name}".*>.*</a>}m
end end
end end
...@@ -81,7 +81,7 @@ describe LabelsHelper do ...@@ -81,7 +81,7 @@ describe LabelsHelper do
['issue', :issue].each do |type| ['issue', :issue].each do |type|
context "set to #{type}" do context "set to #{type}" do
it 'links to correct page' do it 'links to correct page' do
expect(link_to_label(label_presenter, type: type)).to match %r{<a href="/#{label.project.full_path}/#{type.to_s.pluralize}\?label_name%5B%5D=#{label.name}">.*</a>} expect(link_to_label(label_presenter, type: type)).to match %r{<a.*href="/#{label.project.full_path}/#{type.to_s.pluralize}\?label_name%5B%5D=#{label.name}".*>.*</a>}m
end end
end end
end end
...@@ -89,7 +89,7 @@ describe LabelsHelper do ...@@ -89,7 +89,7 @@ describe LabelsHelper do
['merge_request', :merge_request].each do |type| ['merge_request', :merge_request].each do |type|
context "set to #{type}" do context "set to #{type}" do
it 'links to correct page' do it 'links to correct page' do
expect(link_to_label(label_presenter, type: type)).to match %r{<a href="/#{label.project.full_path}/-/#{type.to_s.pluralize}\?label_name%5B%5D=#{label.name}">.*</a>} expect(link_to_label(label_presenter, type: type)).to match %r{<a.*href="/#{label.project.full_path}/-/#{type.to_s.pluralize}\?label_name%5B%5D=#{label.name}".*>.*</a>}m
end end
end end
end end
...@@ -113,7 +113,7 @@ describe LabelsHelper do ...@@ -113,7 +113,7 @@ describe LabelsHelper do
context 'without block' do context 'without block' do
it 'uses render_colored_label as the link content' do it 'uses render_colored_label as the link content' do
expect(self).to receive(:render_colored_label) expect(self).to receive(:render_colored_label)
.with(label_presenter, tooltip: true).and_return('Foo') .with(label_presenter).and_return('Foo')
expect(link_to_label(label_presenter)).to match('Foo') expect(link_to_label(label_presenter)).to match('Foo')
end end
end end
......
...@@ -537,8 +537,10 @@ describe MarkupHelper do ...@@ -537,8 +537,10 @@ describe MarkupHelper do
it 'does not style a label that can not be accessed by current_user' do it 'does not style a label that can not be accessed by current_user' do
project = create(:project, :private) project = create(:project, :private)
label = create_and_format_label(project)
expect(create_and_format_label(project)).to eq("<p>#{label_title}</p>") expect(label).to include("~label_1")
expect(label).not_to match(/span class=.*style=.*/)
end end
end end
......
...@@ -28,7 +28,7 @@ describe Banzai::Filter::LabelReferenceFilter do ...@@ -28,7 +28,7 @@ describe Banzai::Filter::LabelReferenceFilter do
it 'includes default classes' do it 'includes default classes' do
doc = reference_filter("Label #{reference}") doc = reference_filter("Label #{reference}")
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-label has-tooltip' expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-label has-tooltip gl-link gl-label-link'
end end
it 'includes a data-project attribute' do it 'includes a data-project attribute' do
...@@ -66,12 +66,12 @@ describe Banzai::Filter::LabelReferenceFilter do ...@@ -66,12 +66,12 @@ describe Banzai::Filter::LabelReferenceFilter do
describe 'label span element' do describe 'label span element' do
it 'includes default classes' do it 'includes default classes' do
doc = reference_filter("Label #{reference}") doc = reference_filter("Label #{reference}")
expect(doc.css('a span').first.attr('class')).to eq 'badge color-label has-tooltip' expect(doc.css('a span').first.attr('class')).to include 'gl-label-text'
end end
it 'includes a style attribute' do it 'includes a style attribute' do
doc = reference_filter("Label #{reference}") doc = reference_filter("Label #{reference}")
expect(doc.css('a span').first.attr('style')).to match(/\Abackground-color: #\h{6}; color: #\h{6}\z/) expect(doc.css('a span').first.attr('style')).to match(/\Abackground-color: #\h{6}\z/)
end end
end end
...@@ -85,7 +85,7 @@ describe Banzai::Filter::LabelReferenceFilter do ...@@ -85,7 +85,7 @@ describe Banzai::Filter::LabelReferenceFilter do
it 'links with adjacent text' do it 'links with adjacent text' do
doc = reference_filter("Label (#{reference}.)") doc = reference_filter("Label (#{reference}.)")
expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\))) expect(doc.to_html).to match(%r(\(<span.+><a.+><span.+>#{label.name}</span></a></span>\.\)))
end end
it 'ignores invalid label IDs' do it 'ignores invalid label IDs' do
...@@ -109,7 +109,7 @@ describe Banzai::Filter::LabelReferenceFilter do ...@@ -109,7 +109,7 @@ describe Banzai::Filter::LabelReferenceFilter do
it 'links with adjacent text' do it 'links with adjacent text' do
doc = reference_filter("Label (#{reference}).") doc = reference_filter("Label (#{reference}).")
expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\)\.)) expect(doc.to_html).to match(%r(\(<span.+><a.+><span.+>#{label.name}</span></a></span>\)\.))
end end
it 'ignores invalid label names' do it 'ignores invalid label names' do
...@@ -133,7 +133,7 @@ describe Banzai::Filter::LabelReferenceFilter do ...@@ -133,7 +133,7 @@ describe Banzai::Filter::LabelReferenceFilter do
it 'links with adjacent text' do it 'links with adjacent text' do
doc = reference_filter("Label (#{reference}).") doc = reference_filter("Label (#{reference}).")
expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\)\.)) expect(doc.to_html).to match(%r(\(<span.+><a.+><span.+>#{label.name}</span></a></span>\)\.))
end end
it 'ignores invalid label names' do it 'ignores invalid label names' do
...@@ -158,7 +158,7 @@ describe Banzai::Filter::LabelReferenceFilter do ...@@ -158,7 +158,7 @@ describe Banzai::Filter::LabelReferenceFilter do
it 'does not include trailing punctuation', :aggregate_failures do it 'does not include trailing punctuation', :aggregate_failures do
['.', ', ok?', '...', '?', '!', ': is that ok?'].each do |trailing_punctuation| ['.', ', ok?', '...', '?', '!', ': is that ok?'].each do |trailing_punctuation|
doc = filter("Label #{reference}#{trailing_punctuation}") doc = filter("Label #{reference}#{trailing_punctuation}")
expect(doc.to_html).to match(%r(<a.+><span.+>\?g\.fm&amp;</span></a>#{Regexp.escape(trailing_punctuation)})) expect(doc.to_html).to match(%r(<span.+><a.+><span.+>\?g\.fm&amp;</span></a></span>#{Regexp.escape(trailing_punctuation)}))
end end
end end
...@@ -184,7 +184,7 @@ describe Banzai::Filter::LabelReferenceFilter do ...@@ -184,7 +184,7 @@ describe Banzai::Filter::LabelReferenceFilter do
it 'links with adjacent text' do it 'links with adjacent text' do
doc = reference_filter("Label (#{reference}.)") doc = reference_filter("Label (#{reference}.)")
expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\))) expect(doc.to_html).to match(%r(\(<span.+><a.+><span.+>#{label.name}</span></a></span>\.\)))
end end
it 'ignores invalid label names' do it 'ignores invalid label names' do
...@@ -208,7 +208,7 @@ describe Banzai::Filter::LabelReferenceFilter do ...@@ -208,7 +208,7 @@ describe Banzai::Filter::LabelReferenceFilter do
it 'links with adjacent text' do it 'links with adjacent text' do
doc = reference_filter("Label (#{reference}.)") doc = reference_filter("Label (#{reference}.)")
expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\))) expect(doc.to_html).to match(%r(\(<span.+><a.+><span.+>#{label.name}</span></a></span>\.\)))
end end
it 'ignores invalid label names' do it 'ignores invalid label names' do
...@@ -232,7 +232,7 @@ describe Banzai::Filter::LabelReferenceFilter do ...@@ -232,7 +232,7 @@ describe Banzai::Filter::LabelReferenceFilter do
it 'links with adjacent text' do it 'links with adjacent text' do
doc = reference_filter("Label (#{reference}.)") doc = reference_filter("Label (#{reference}.)")
expect(doc.to_html).to match(%r(\(<a.+><span.+>g\.fm &amp; references\?</span></a>\.\))) expect(doc.to_html).to match(%r(\(<span.+><a.+><span.+>g\.fm &amp; references\?</span></a></span>\.\)))
end end
it 'ignores invalid label names' do it 'ignores invalid label names' do
...@@ -320,7 +320,7 @@ describe Banzai::Filter::LabelReferenceFilter do ...@@ -320,7 +320,7 @@ describe Banzai::Filter::LabelReferenceFilter do
it 'links with adjacent text' do it 'links with adjacent text' do
doc = reference_filter("Label (#{reference}.)") doc = reference_filter("Label (#{reference}.)")
expect(doc.to_html).to match(%r(\(<a.+>Label</a>\.\))) expect(doc.to_html).to match(%r(\(<span.+><a.+>Label</a></span>\.\)))
end end
it 'includes a data-project attribute' do it 'includes a data-project attribute' do
...@@ -358,7 +358,7 @@ describe Banzai::Filter::LabelReferenceFilter do ...@@ -358,7 +358,7 @@ describe Banzai::Filter::LabelReferenceFilter do
it 'links with adjacent text' do it 'links with adjacent text' do
doc = reference_filter("Label (#{reference}.)") doc = reference_filter("Label (#{reference}.)")
expect(doc.to_html).to match(%r(\(<a.+><span.+>#{group_label.name}</span></a>\.\))) expect(doc.to_html).to match(%r(\(<span.+><a.+><span.+>#{group_label.name}</span></a></span>\.\)))
end end
it 'ignores invalid label names' do it 'ignores invalid label names' do
...@@ -381,7 +381,7 @@ describe Banzai::Filter::LabelReferenceFilter do ...@@ -381,7 +381,7 @@ describe Banzai::Filter::LabelReferenceFilter do
it 'links with adjacent text' do it 'links with adjacent text' do
doc = reference_filter("Label (#{reference}.)") doc = reference_filter("Label (#{reference}.)")
expect(doc.to_html).to match(%r(\(<a.+><span.+>#{group_label.name}</span></a>\.\))) expect(doc.to_html).to match(%r(\(<span.+><a.+><span.+>#{group_label.name}</span></a></span>\.\)))
end end
it 'ignores invalid label names' do it 'ignores invalid label names' do
......
...@@ -3,65 +3,11 @@ ...@@ -3,65 +3,11 @@
require 'spec_helper' require 'spec_helper'
describe ResourceEvents::ChangeMilestoneService do describe ResourceEvents::ChangeMilestoneService do
shared_examples 'milestone events creator' do it_behaves_like 'a milestone events creator' do
let_it_be(:user) { create(:user) } let(:resource) { create(:issue) }
let_it_be(:milestone) { create(:milestone) }
context 'when milestone is present' do
before do
resource.milestone = milestone
end
let(:service) { described_class.new(resource: resource, user: user, created_at: created_at_time) }
it 'creates the expected event record' do
expect { service.execute }.to change { ResourceMilestoneEvent.count }.from(0).to(1)
events = ResourceMilestoneEvent.all
expect(events.size).to eq(1)
expect_event_record(events.first, action: 'add', milestone: milestone, state: 'opened')
end
end
context 'when milestones is not present' do
before do
resource.milestone = nil
end
let(:service) { described_class.new(resource: resource, user: user, created_at: created_at_time) }
it 'creates the expected event records' do
expect { service.execute }.to change { ResourceMilestoneEvent.count }.from(0).to(1)
expect_event_record(ResourceMilestoneEvent.first, action: 'remove', milestone: nil, state: 'opened')
end
end
def expect_event_record(event, expected_attrs)
expect(event.action).to eq(expected_attrs[:action])
expect(event.state).to eq(expected_attrs[:state])
expect(event.user).to eq(user)
expect(event.issue).to eq(resource) if resource.is_a?(Issue)
expect(event.issue).to be_nil unless resource.is_a?(Issue)
expect(event.merge_request).to eq(resource) if resource.is_a?(MergeRequest)
expect(event.merge_request).to be_nil unless resource.is_a?(MergeRequest)
expect(event.milestone).to eq(expected_attrs[:milestone])
expect(event.created_at).to eq(created_at_time)
end
end
let_it_be(:merge_request) { create(:merge_request) }
let_it_be(:issue) { create(:issue) }
let!(:created_at_time) { Time.utc(2019, 12, 30) }
it_behaves_like 'milestone events creator' do
let(:resource) { issue }
end end
it_behaves_like 'milestone events creator' do it_behaves_like 'a milestone events creator' do
let(:resource) { merge_request } let(:resource) { create(:merge_request) }
end end
end end
# frozen_string_literal: true
shared_examples 'a milestone events creator' do
let_it_be(:user) { create(:user) }
let(:created_at_time) { Time.utc(2019, 12, 30) }
let(:service) { described_class.new(resource, user, created_at: created_at_time) }
context 'when milestone is present' do
let_it_be(:milestone) { create(:milestone) }
before do
resource.milestone = milestone
end
it 'creates the expected event record' do
expect { service.execute }.to change { ResourceMilestoneEvent.count }.by(1)
expect_event_record(ResourceMilestoneEvent.last, action: 'add', milestone: milestone, state: 'opened')
end
end
context 'when milestones is not present' do
before do
resource.milestone = nil
end
it 'creates the expected event records' do
expect { service.execute }.to change { ResourceMilestoneEvent.count }.by(1)
expect_event_record(ResourceMilestoneEvent.last, action: 'remove', milestone: nil, state: 'opened')
end
end
def expect_event_record(event, expected_attrs)
expect(event.action).to eq(expected_attrs[:action])
expect(event.state).to eq(expected_attrs[:state])
expect(event.user).to eq(user)
expect(event.issue).to eq(resource) if resource.is_a?(Issue)
expect(event.issue).to be_nil unless resource.is_a?(Issue)
expect(event.merge_request).to eq(resource) if resource.is_a?(MergeRequest)
expect(event.merge_request).to be_nil unless resource.is_a?(MergeRequest)
expect(event.milestone).to eq(expected_attrs[:milestone])
expect(event.created_at).to eq(created_at_time)
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment