summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-02-25 15:08:50 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-02-25 15:08:50 +0000
commite06d0e779673d745972863302858105aad9032e5 (patch)
tree0ff35b27a949a164f586613004b4abfe33e7d20e
parentf7dae0cdcb70ecb71c1d65f099e9d96b27a4548c (diff)
downloadgitlab-ce-e06d0e779673d745972863302858105aad9032e5.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/boards/components/board.js11
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js8
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js10
-rw-r--r--app/assets/javascripts/lib/utils/highlight.js7
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js4
-rw-r--r--app/assets/javascripts/lib/utils/unit_format/formatter_factory.js119
-rw-r--r--app/assets/javascripts/lib/utils/unit_format/index.js167
-rw-r--r--app/assets/javascripts/monitoring/components/charts/time_series.vue9
-rw-r--r--app/assets/javascripts/notes/components/discussion_resolve_button.vue12
-rw-r--r--app/assets/stylesheets/framework/modal.scss10
-rw-r--r--app/assets/stylesheets/framework/spinner.scss3
-rw-r--r--app/assets/stylesheets/pages/issuable.scss39
-rw-r--r--app/assets/stylesheets/pages/labels.scss27
-rw-r--r--app/assets/stylesheets/pages/milestone.scss16
-rw-r--r--app/assets/stylesheets/pages/notes.scss12
-rw-r--r--app/helpers/labels_helper.rb82
-rw-r--r--app/models/clusters/cluster.rb18
-rw-r--r--app/services/issuable/common_system_notes_service.rb2
-rw-r--r--app/services/resource_events/change_milestone_service.rb2
-rw-r--r--app/views/projects/issues/_issue.html.haml2
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml2
-rw-r--r--app/views/shared/_delete_label_modal.html.haml2
-rw-r--r--app/views/shared/boards/components/_board.html.haml13
-rw-r--r--app/views/shared/boards/components/sidebar/_labels.html.haml15
-rw-r--r--app/views/shared/milestones/_issuable.html.haml2
-rw-r--r--app/views/shared/milestones/_labels_tab.html.haml8
-rw-r--r--changelogs/unreleased/196648-replace-_-with-lodash.yml5
-rw-r--r--changelogs/unreleased/38144-replace-labels-in-haml-with-gitlab-ui-css.yml5
-rw-r--r--changelogs/unreleased/Resolve-Migrate--fa-spinner-app-assets-javascripts-notes-components-discu.yml5
-rw-r--r--lib/banzai/filter/label_reference_filter.rb17
-rw-r--r--lib/banzai/filter/reference_filter.rb3
-rw-r--r--lib/gitlab/markdown_cache.rb2
-rw-r--r--locale/gitlab.pot12
-rw-r--r--spec/features/boards/sidebar_spec.rb10
-rw-r--r--spec/features/container_registry_spec.rb75
-rw-r--r--spec/features/groups/container_registry_spec.rb93
-rw-r--r--spec/features/issuables/issuable_list_spec.rb8
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb2
-rw-r--r--spec/features/labels_hierarchy_spec.rb6
-rw-r--r--spec/features/projects/container_registry_spec.rb161
-rw-r--r--spec/frontend/lib/utils/unit_format/formatter_factory_spec.js200
-rw-r--r--spec/frontend/lib/utils/unit_format/index_spec.js117
-rw-r--r--spec/helpers/labels_helper_spec.rb12
-rw-r--r--spec/helpers/markup_helper_spec.rb4
-rw-r--r--spec/lib/banzai/filter/label_reference_filter_spec.rb26
-rw-r--r--spec/services/resource_events/change_milestone_service_spec.rb62
-rw-r--r--spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb46
47 files changed, 1199 insertions, 274 deletions
diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js
index a6deb656b37..67046715e9b 100644
--- a/app/assets/javascripts/boards/components/board.js
+++ b/app/assets/javascripts/boards/components/board.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import Sortable from 'sortablejs';
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 { s__, __, sprintf } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
@@ -14,6 +14,7 @@ import IssueCount from './issue_count.vue';
import boardsStore from '../stores/boards_store';
import { getBoardSortableDefaultOptions, sortableEnd } from '../mixins/sortable_default_options';
import { ListType } from '../constants';
+import { isScopedLabel } from '~/lib/utils/common_utils';
export default Vue.extend({
components: {
@@ -24,6 +25,7 @@ export default Vue.extend({
GlButtonGroup,
IssueCount,
GlButton,
+ GlLabel,
GlTooltip,
},
directives: {
@@ -95,6 +97,9 @@ export default Vue.extend({
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
return `boards.${this.boardId}.${this.list.type}.${this.list.id}`;
},
+ helpLink() {
+ return boardsStore.scopedLabels.helpLink;
+ },
},
watch: {
filter: {
@@ -145,6 +150,10 @@ export default Vue.extend({
}
},
methods: {
+ showScopedLabels(label) {
+ return boardsStore.scopedLabels.enabled && isScopedLabel(label);
+ },
+
showNewIssueForm() {
this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm;
},
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index ba1fe9202fc..9b67126bee2 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -2,6 +2,7 @@
import $ from 'jquery';
import Vue from 'vue';
+import { GlLabel } from '@gitlab/ui';
import Flash from '~/flash';
import { sprintf, __ } from '~/locale';
import Sidebar from '~/right_sidebar';
@@ -22,6 +23,7 @@ export default Vue.extend({
components: {
AssigneeTitle,
Assignees,
+ GlLabel,
SidebarEpicsSelect: () =>
import('ee_component/sidebar/components/sidebar_item_epics_select.vue'),
RemoveBtn,
@@ -67,6 +69,9 @@ export default Vue.extend({
selectedLabels() {
return this.hasLabels ? this.issue.labels.map(l => l.title).join(',') : '';
},
+ helpLink() {
+ return boardsStore.scopedLabels.helpLink;
+ },
},
watch: {
detail: {
@@ -147,8 +152,5 @@ export default Vue.extend({
showScopedLabels(label) {
return boardsStore.scopedLabels.enabled && isScopedLabel(label);
},
- helpLink() {
- return boardsStore.scopedLabels.helpLink;
- },
},
});
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index ad8095e1ae3..a70bab013c6 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-import _ from 'underscore';
+import { isString, mapValues, isNumber, reduce } from 'lodash';
import * as timeago from 'timeago.js';
import dateFormat from 'dateformat';
import { languageCode, s__, __, n__ } from '../../locale';
@@ -79,7 +79,7 @@ export const getDayName = date =>
* @returns {String}
*/
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'));
}
return dateFormat(datetime, format);
@@ -497,7 +497,7 @@ export const parseSeconds = (
let unorderedMinutes = Math.abs(seconds / SECONDS_PER_MINUTE);
- return _.mapObject(timePeriodConstraints, minutesPerPeriod => {
+ return mapValues(timePeriodConstraints, minutesPerPeriod => {
if (minutesPerPeriod === 0) {
return 0;
}
@@ -516,7 +516,7 @@ export const parseSeconds = (
* If the 'fullNameFormat' param is passed it returns a non condensed string eg '1 week 3 days'
*/
export const stringifyTime = (timeObject, fullNameFormat = false) => {
- const reducedTime = _.reduce(
+ const reducedTime = reduce(
timeObject,
(memo, unitValue, unitName) => {
const isNonZero = Boolean(unitValue);
@@ -642,7 +642,7 @@ export const dayAfter = date => new Date(newDate(date).setDate(date.getDate() +
* @return {String} approximated time
*/
export const approximateDuration = (seconds = 0) => {
- if (!_.isNumber(seconds) || seconds < 0) {
+ if (!isNumber(seconds) || seconds < 0) {
return '';
}
diff --git a/app/assets/javascripts/lib/utils/highlight.js b/app/assets/javascripts/lib/utils/highlight.js
index 8f0afa3467d..b1dd562f63a 100644
--- a/app/assets/javascripts/lib/utils/highlight.js
+++ b/app/assets/javascripts/lib/utils/highlight.js
@@ -1,5 +1,4 @@
import fuzzaldrinPlus from 'fuzzaldrin-plus';
-import _ from 'underscore';
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
*/
export default function highlight(string, match = '', matchPrefix = '<b>', matchSuffix = '</b>') {
- if (_.isUndefined(string) || _.isNull(string)) {
+ if (!string) {
return '';
}
- if (_.isUndefined(match) || _.isNull(match) || match === '') {
+ if (!match) {
return string;
}
@@ -34,7 +33,7 @@ export default function highlight(string, match = '', matchPrefix = '<b>', match
return sanitizedValue
.split('')
.map((character, i) => {
- if (_.contains(occurrences, i)) {
+ if (occurrences.includes(i)) {
return `${matchPrefix}${character}${matchSuffix}`;
}
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index a03fedcd7e7..9ed286826cc 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -1,4 +1,4 @@
-import _ from 'underscore';
+import { isString } from 'lodash';
/**
* Adds a , to a string composed by numbers, at every 3 chars.
@@ -199,7 +199,7 @@ export const splitCamelCase = string =>
* i.e. "My Group / My Subgroup / My Project"
*/
export const truncateNamespace = (string = '') => {
- if (_.isNull(string) || !_.isString(string)) {
+ if (string === null || !isString(string)) {
return '';
}
diff --git a/app/assets/javascripts/lib/utils/unit_format/formatter_factory.js b/app/assets/javascripts/lib/utils/unit_format/formatter_factory.js
new file mode 100644
index 00000000000..432a9254558
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/unit_format/formatter_factory.js
@@ -0,0 +1,119 @@
+/**
+ * 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);
+};
diff --git a/app/assets/javascripts/lib/utils/unit_format/index.js b/app/assets/javascripts/lib/utils/unit_format/index.js
new file mode 100644
index 00000000000..daf70ebb5d7
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/unit_format/index.js
@@ -0,0 +1,167 @@
+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`);
+};
diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue
index 8abb16f58ca..1c39fb072d9 100644
--- a/app/assets/javascripts/monitoring/components/charts/time_series.vue
+++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue
@@ -4,7 +4,7 @@ import { GlLink, GlButton, GlTooltip, GlResizeObserverDirective } from '@gitlab/
import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
import dateFormat from 'dateformat';
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 Icon from '~/vue_shared/components/icon.vue';
import {
@@ -37,6 +37,8 @@ const events = {
datazoom: 'datazoom',
};
+const yValFormatter = getFormatter('number');
+
export default {
components: {
GlAreaChart,
@@ -171,7 +173,7 @@ export default {
boundaryGap: [0.1, 0.1],
scale: true,
axisLabel: {
- formatter: num => roundOffFloat(num, 3).toString(),
+ formatter: num => yValFormatter(num, 3),
},
...yAxis,
};
@@ -313,7 +315,8 @@ export default {
this.tooltip.commitUrl = deploy.commitUrl;
} else {
const { seriesName, color, dataIndex } = dataPoint;
- const value = yVal.toFixed(3);
+ const value = yValFormatter(yVal, 3);
+
this.tooltip.content.push({
name: seriesName,
dataIndex,
diff --git a/app/assets/javascripts/notes/components/discussion_resolve_button.vue b/app/assets/javascripts/notes/components/discussion_resolve_button.vue
index 2b29d710236..77f6f1e51c5 100644
--- a/app/assets/javascripts/notes/components/discussion_resolve_button.vue
+++ b/app/assets/javascripts/notes/components/discussion_resolve_button.vue
@@ -1,6 +1,11 @@
<script>
+import { GlLoadingIcon } from '@gitlab/ui';
+
export default {
name: 'ResolveDiscussionButton',
+ components: {
+ GlLoadingIcon,
+ },
props: {
isResolving: {
type: Boolean,
@@ -17,12 +22,7 @@ export default {
<template>
<button ref="button" type="button" class="btn btn-default ml-sm-2" @click="$emit('onClick')">
- <i
- v-if="isResolving"
- ref="isResolvingIcon"
- aria-hidden="true"
- class="fa fa-spinner fa-spin"
- ></i>
+ <gl-loading-icon v-if="isResolving" ref="isResolvingIcon" inline />
{{ buttonTitle }}
</button>
</template>
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index ac8437c23ca..f8c46a4495e 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -13,16 +13,14 @@
.page-title,
.modal-title {
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
.modal-title-with-label span {
vertical-align: middle;
display: inline-block;
}
-
- .color-label {
- font-size: $gl-font-size;
- padding: $gl-vert-padding $label-padding-modal;
- vertical-align: middle;
- }
}
.modal-title {
diff --git a/app/assets/stylesheets/framework/spinner.scss b/app/assets/stylesheets/framework/spinner.scss
index 5e05311041c..b7a99d421c9 100644
--- a/app/assets/stylesheets/framework/spinner.scss
+++ b/app/assets/stylesheets/framework/spinner.scss
@@ -51,7 +51,8 @@
}
.btn {
- .spinner {
+ .spinner,
+ .gl-spinner {
vertical-align: text-bottom;
}
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 43636f65eb8..fd56f655c0a 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -86,14 +86,19 @@
}
.issuable-show-labels {
- a {
+ .gl-label {
margin-bottom: 5px;
margin-right: 5px;
+ }
+
+ a {
display: inline-block;
.color-label {
padding: 4px $grid-size;
border-radius: $label-border-radius;
+ margin-right: 4px;
+ margin-bottom: 4px;
}
&:hover .color-label {
@@ -159,9 +164,25 @@
.avatar {
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 {
@@ -800,11 +821,23 @@
a {
color: $gl-text-color;
+ }
- .fa {
- color: $gl-text-color-secondary;
+ .gl-label-link {
+ 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) {
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 7d5e185834b..91ac150f6e2 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -127,6 +127,11 @@
.color-label {
padding: $gl-padding-4 $grid-size;
}
+
+ .prepend-description-left {
+ vertical-align: top;
+ line-height: 24px;
+ }
}
.prioritized-labels {
@@ -305,10 +310,13 @@
width: 150px;
flex-shrink: 0;
- .badge {
- overflow: hidden;
- text-overflow: ellipsis;
- max-width: 100%;
+ .scoped-label-wrapper,
+ .gl-label {
+ line-height: $gl-line-height;
+ }
+
+ .gl-label-scoped .gl-label-text:last-of-type {
+ padding-right: 22px;
}
}
@@ -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
.modal-header .page-title {
.scoped-label-wrapper {
- .scoped-label {
+ .scoped-label,
+ .gl-label-icon {
line-height: 20px;
}
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index b399662997c..cd1154b88a5 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -59,9 +59,19 @@ $status-box-line-height: 26px;
}
.issuable-row {
- span a {
- color: $gl-text-color;
- word-wrap: break-word;
+ span {
+ a {
+ color: $gl-text-color;
+ word-wrap: break-word;
+ }
+
+ .gl-label-link {
+ color: inherit;
+ }
+
+ .gl-label-icon {
+ color: $gray-500;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 347addcec37..aaecbd6ff00 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -283,7 +283,7 @@ $note-form-margin-left: 72px;
text-transform: lowercase;
}
- a {
+ a:not(.gl-link) {
color: $blue-600;
}
@@ -671,6 +671,16 @@ $note-form-margin-left: 72px;
a:hover {
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;
+ }
+ }
}
/**
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index 91127df318e..97232def91c 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -36,37 +36,42 @@ module LabelsHelper
# link_to_label(label) { "My Custom Label Text" }
#
# 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)
if block_given?
- link_to link, class: css_class, &block
+ link_to link, &block
else
- render_label(label, tooltip: tooltip, link: link, css: css_class)
+ render_label(label, link: link, tooltip: tooltip, small: small)
end
end
- def render_label(label, tooltip: true, link: nil, css: nil, dataset: nil)
- # if scoped label is used then EE wraps label tag with scoped label
- # doc link
- html = render_colored_label(label, tooltip: tooltip)
- html = link_to(html, link, class: css, data: dataset) if link
+ def render_label(label, link: nil, tooltip: true, dataset: nil, small: false)
+ html = render_colored_label(label)
- 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
- def render_colored_label(label, label_suffix: '', tooltip: true, title: nil)
- text_color = text_color_for_bg(label.color)
- title ||= tooltip ? label_tooltip_title(label) : label.name
+ def render_colored_label(label, suffix: '')
+ render_label_text(
+ 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
- # by LabelReferenceFilter
- span = %(<span class="badge color-label #{"has-tooltip" if tooltip}" ) +
- %(data-html="true" style="background-color: #{label.color}; color: #{text_color}" ) +
- %(title="#{ERB::Util.html_escape_once(title)}" data-container="body">) +
- %(#{ERB::Util.html_escape_once(label.name)}#{label_suffix}</span>)
+ # We need the `label` argument here for EE
+ def wrap_label_html(label_html, small:, label:)
+ wrapper_classes = %w(gl-label)
+ wrapper_classes << 'gl-label-sm' if small
- span.html_safe
+ %(<span class="#{wrapper_classes.join(' ')}">#{label_html}</span>).html_safe
end
def label_tooltip_title(label)
@@ -109,6 +114,20 @@ module LabelsHelper
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)
if bg_color.length == 4
r, g, b = bg_color[1, 4].scan(/./).map { |v| (v * 2).hex }
@@ -246,6 +265,31 @@ module LabelsHelper
def issuable_types
['issues', 'merge requests']
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
LabelsHelper.prepend_if_ee('EE::LabelsHelper')
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 7e76d324bdc..6e890de924e 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -11,15 +11,15 @@ module Clusters
self.table_name = 'clusters'
APPLICATIONS = {
- Applications::Helm.application_name => Applications::Helm,
- Applications::Ingress.application_name => Applications::Ingress,
- Applications::CertManager.application_name => Applications::CertManager,
- Applications::Crossplane.application_name => Applications::Crossplane,
- Applications::Prometheus.application_name => Applications::Prometheus,
- Applications::Runner.application_name => Applications::Runner,
- Applications::Jupyter.application_name => Applications::Jupyter,
- Applications::Knative.application_name => Applications::Knative,
- Applications::ElasticStack.application_name => Applications::ElasticStack
+ Clusters::Applications::Helm.application_name => Clusters::Applications::Helm,
+ Clusters::Applications::Ingress.application_name => Clusters::Applications::Ingress,
+ Clusters::Applications::CertManager.application_name => Clusters::Applications::CertManager,
+ Clusters::Applications::Crossplane.application_name => Clusters::Applications::Crossplane,
+ Clusters::Applications::Prometheus.application_name => Clusters::Applications::Prometheus,
+ Clusters::Applications::Runner.application_name => Clusters::Applications::Runner,
+ Clusters::Applications::Jupyter.application_name => Clusters::Applications::Jupyter,
+ Clusters::Applications::Knative.application_name => Clusters::Applications::Knative,
+ Clusters::Applications::ElasticStack.application_name => Clusters::Applications::ElasticStack
}.freeze
DEFAULT_ENVIRONMENT = '*'
KUBE_INGRESS_BASE_DOMAIN = 'KUBE_INGRESS_BASE_DOMAIN'
diff --git a/app/services/issuable/common_system_notes_service.rb b/app/services/issuable/common_system_notes_service.rb
index c53e19c922a..67cf212691f 100644
--- a/app/services/issuable/common_system_notes_service.rb
+++ b/app/services/issuable/common_system_notes_service.rb
@@ -101,7 +101,7 @@ module Issuable
def create_milestone_note
if milestone_changes_tracking_enabled?
# Creates a synthetic note
- ResourceEvents::ChangeMilestoneService.new(resource: issuable, user: current_user).execute
+ ResourceEvents::ChangeMilestoneService.new(issuable, current_user).execute
else
SystemNoteService.change_milestone(issuable, issuable.project, current_user, issuable.milestone)
end
diff --git a/app/services/resource_events/change_milestone_service.rb b/app/services/resource_events/change_milestone_service.rb
index dd637bcc765..ea196822f74 100644
--- a/app/services/resource_events/change_milestone_service.rb
+++ b/app/services/resource_events/change_milestone_service.rb
@@ -4,7 +4,7 @@ module ResourceEvents
class ChangeMilestoneService
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
@user = user
@event_created_at = created_at
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index c8ab47888d0..a6c6b77c9dd 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -38,7 +38,7 @@
- if issue.labels.any?
&nbsp;
- 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
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 36f19ee6175..744dca1c462 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -35,7 +35,7 @@
- if merge_request.labels.any?
&nbsp;
- 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
%ul.controls.d-flex.align-items-end
diff --git a/app/views/shared/_delete_label_modal.html.haml b/app/views/shared/_delete_label_modal.html.haml
index f37dd2cdf02..c6629cd33a5 100644
--- a/app/views/shared/_delete_label_modal.html.haml
+++ b/app/views/shared/_delete_label_modal.html.haml
@@ -2,7 +2,7 @@
.modal-dialog
.modal-content
.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') }
%span{ "aria-hidden": true } &times;
diff --git a/app/views/shared/boards/components/_board.html.haml b/app/views/shared/boards/components/_board.html.haml
index 3db96db73ce..e42d8650708 100644
--- a/app/views/shared/boards/components/_board.html.haml
+++ b/app/views/shared/boards/components/_board.html.haml
@@ -29,11 +29,14 @@
":title" => '(list.assignee && 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\"",
- ":title" => '(list.label ? list.label.description : "")',
- data: { container: "body", placement: "bottom" },
- ":style" => "{ backgroundColor: (list.label && list.label.color ? list.label.color : null), color: (list.label && list.label.textColor ? list.label.textColor : \"#2e2e2e\") }" }
- {{ list.title }}
+ %gl-label{ "v-if" => " list.type === \"label\"",
+ ":background-color" => "list.label.color",
+ ":title" => "list.label.title",
+ ":description" => "list.label.description",
+ "tooltipPlacement" => "bottom",
+ ":size" => '(!list.isExpanded ? "sm" : "")',
+ ":scoped" => "showScopedLabels(list.label)",
+ ":scoped-labels-documentation-link" => "helpLink" }
- if can?(current_user, :admin_list, current_board_parent)
%board-delete{ "inline-template" => true,
diff --git a/app/views/shared/boards/components/sidebar/_labels.html.haml b/app/views/shared/boards/components/sidebar/_labels.html.haml
index c50826a7cda..a1088dc5222 100644
--- a/app/views/shared/boards/components/sidebar/_labels.html.haml
+++ b/app/views/shared/boards/components/sidebar/_labels.html.haml
@@ -8,15 +8,12 @@
%span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" }
= _("None")
%span{ "v-for" => "label in issue.labels" }
- %span.d-inline-block.position-relative.scoped-label-wrapper{ "v-if" => "showScopedLabels(label)" }
- %a{ href: '#' }
- %span.badge.color-label.label{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" }
- {{ label.title }}
- %a.label.scoped-label{ ":href" => "helpLink()" }
- %i.fa.fa-question-circle{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" }
- %a{ href: "#", "v-else" => true }
- .badge.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" }
- {{ label.title }}
+ %gl-label{ ":key" => "label.id",
+ ":background-color" => "label.color",
+ ":title" => "label.title",
+ ":description" => "label.description",
+ ":scoped" => "showScopedLabels(label)",
+ ":scoped-labels-documentation-link" => "helpLink" }
- if can_admin_issue?
.selectbox
diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml
index ae3ab2adfd0..965c72b82ba 100644
--- a/app/views/shared/milestones/_issuable.html.haml
+++ b/app/views/shared/milestones/_issuable.html.haml
@@ -21,7 +21,7 @@
%span.issuable-number= issuable.to_reference
- 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
- assignees.each do |assignee|
diff --git a/app/views/shared/milestones/_labels_tab.html.haml b/app/views/shared/milestones/_labels_tab.html.haml
index ecab037e378..4c930b90ce7 100644
--- a/app/views/shared/milestones/_labels_tab.html.haml
+++ b/app/views/shared/milestones/_labels_tab.html.haml
@@ -3,11 +3,9 @@
- options = { milestone_title: @milestone.title, label_name: label.title }
%li.no-border
- %span.label-row
- %span.label-name
- = render_label(label, tooltip: false, link: milestones_label_path(options))
- %span.prepend-description-left
- = markdown_field(label, :description)
+ = render_label(label, tooltip: false, link: milestones_label_path(options))
+ %span.prepend-description-left
+ = markdown_field(label, :description)
.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
diff --git a/changelogs/unreleased/196648-replace-_-with-lodash.yml b/changelogs/unreleased/196648-replace-_-with-lodash.yml
new file mode 100644
index 00000000000..397e2ded94d
--- /dev/null
+++ b/changelogs/unreleased/196648-replace-_-with-lodash.yml
@@ -0,0 +1,5 @@
+---
+title: Replaced underscore with lodash for app/assets/javascripts/lib
+merge_request: 25042
+author: Shubham Pandey
+type: other
diff --git a/changelogs/unreleased/38144-replace-labels-in-haml-with-gitlab-ui-css.yml b/changelogs/unreleased/38144-replace-labels-in-haml-with-gitlab-ui-css.yml
new file mode 100644
index 00000000000..fcb22a03e9f
--- /dev/null
+++ b/changelogs/unreleased/38144-replace-labels-in-haml-with-gitlab-ui-css.yml
@@ -0,0 +1,5 @@
+---
+title: New styles for scoped labels
+merge_request: 21377
+author:
+type: changed
diff --git a/changelogs/unreleased/Resolve-Migrate--fa-spinner-app-assets-javascripts-notes-components-discu.yml b/changelogs/unreleased/Resolve-Migrate--fa-spinner-app-assets-javascripts-notes-components-discu.yml
new file mode 100644
index 00000000000..1506f672ed2
--- /dev/null
+++ b/changelogs/unreleased/Resolve-Migrate--fa-spinner-app-assets-javascripts-notes-components-discu.yml
@@ -0,0 +1,5 @@
+---
+title: Migrate .fa-spinner to .spinner for app/assets/javascripts/notes/components/discussion_resolve_button.vue
+merge_request: 25055
+author: nuwe1
+type: other
diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb
index 609ea8fb5ca..60ffb178393 100644
--- a/lib/banzai/filter/label_reference_filter.rb
+++ b/lib/banzai/filter/label_reference_filter.rb
@@ -93,23 +93,26 @@ module Banzai
end
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
- def tooltip_title(label)
- nil
+ def wrap_link(link, label)
+ presenter = label.present(issuable_subject: project || group)
+ LabelsHelper.wrap_label_html(link, small: true, label: presenter)
end
def full_path_ref?(matches)
matches[:namespace] && matches[:project]
end
+ def reference_class(type, tooltip: true)
+ super + ' gl-link gl-label-link'
+ end
+
def object_link_title(object, matches)
- # use title of wrapped element instead
- nil
+ presenter = object.present(issuable_subject: project || group)
+ LabelsHelper.label_tooltip_title(presenter)
end
end
end
end
-
-Banzai::Filter::LabelReferenceFilter.prepend_if_ee('EE::Banzai::Filter::LabelReferenceFilter')
diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb
index b3ce9200b49..38bbed3cf72 100644
--- a/lib/banzai/filter/reference_filter.rb
+++ b/lib/banzai/filter/reference_filter.rb
@@ -37,7 +37,8 @@ module Banzai
attributes[:reference_type] ||= self.class.reference_type
attributes[:container] ||= 'body'
- attributes[:placement] ||= 'bottom'
+ attributes[:placement] ||= 'top'
+ attributes[:html] ||= 'true'
attributes.delete(:original) if context[:no_original_data]
attributes.map do |key, value|
%Q(data-#{key.to_s.dasherize}="#{escape_once(value)}")
diff --git a/lib/gitlab/markdown_cache.rb b/lib/gitlab/markdown_cache.rb
index 3dfaec48311..473c7203ead 100644
--- a/lib/gitlab/markdown_cache.rb
+++ b/lib/gitlab/markdown_cache.rb
@@ -3,7 +3,7 @@
module Gitlab
module MarkdownCache
# Increment this number every time the renderer changes its output
- CACHE_COMMONMARK_VERSION = 18
+ CACHE_COMMONMARK_VERSION = 19
CACHE_COMMONMARK_VERSION_START = 10
BaseError = Class.new(StandardError)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 4c90b213539..04fd7d3cc1a 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -5848,6 +5848,9 @@ msgstr ""
msgid "CustomCycleAnalytics|Select stop event"
msgstr ""
+msgid "CustomCycleAnalytics|Stage name already exists"
+msgstr ""
+
msgid "CustomCycleAnalytics|Start event"
msgstr ""
@@ -18564,9 +18567,6 @@ msgstr ""
msgid "Subscription deletion failed."
msgstr ""
-msgid "Subscription successfully applied to \"%{group_name}\""
-msgstr ""
-
msgid "Subscription successfully created."
msgstr ""
@@ -20756,6 +20756,12 @@ msgstr ""
msgid "Uninstalling"
msgstr ""
+msgid "Units|ms"
+msgstr ""
+
+msgid "Units|s"
+msgstr ""
+
msgid "Unknown"
msgstr ""
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index c7edb574f19..e54c11f657d 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -277,7 +277,7 @@ describe 'Issue Boards', :js do
wait_for_requests
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(stretch.title)
end
@@ -299,7 +299,7 @@ describe 'Issue Boards', :js do
find('.dropdown-menu-close-icon').click
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)
end
end
@@ -328,7 +328,7 @@ describe 'Issue Boards', :js do
find('.dropdown-menu-close-icon').click
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(regression.title)
end
@@ -357,13 +357,13 @@ describe 'Issue Boards', :js do
find('.dropdown-menu-close-icon').click
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)
end
end
# '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)
end
diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb
deleted file mode 100644
index 881cad1864b..00000000000
--- a/spec/features/container_registry_spec.rb
+++ /dev/null
@@ -1,75 +0,0 @@
-# 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: [])
- 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
-
- def visit_container_registry
- visit project_container_registry_index_path(project)
- end
-end
diff --git a/spec/features/groups/container_registry_spec.rb b/spec/features/groups/container_registry_spec.rb
new file mode 100644
index 00000000000..7e3c1728f3c
--- /dev/null
+++ b/spec/features/groups/container_registry_spec.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Container Registry', :js do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:project) { create(:project, namespace: group) }
+
+ let(:container_repository) do
+ create(:container_repository, name: 'my/image')
+ end
+
+ before do
+ group.add_owner(user)
+ sign_in(user)
+ stub_container_registry_config(enabled: true)
+ stub_container_registry_tags(repository: :any, tags: [])
+ 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 'list page has no container title' do
+ visit_container_registry
+
+ expect(page).to have_content _('There are no container images available in this group')
+ 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 'image repository delete is disabled' do
+ visit_container_registry
+
+ 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')
+
+ 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
+
+ def visit_container_registry
+ visit group_container_registries_path(group)
+ end
+
+ def visit_container_registry_details(name)
+ visit_container_registry
+ click_link(name)
+ end
+end
diff --git a/spec/features/issuables/issuable_list_spec.rb b/spec/features/issuables/issuable_list_spec.rb
index bcc05d313ad..7014a51ccdc 100644
--- a/spec/features/issuables/issuable_list_spec.rb
+++ b/spec/features/issuables/issuable_list_spec.rb
@@ -41,10 +41,10 @@ describe 'issuable list' do
visit_issuable_list(issuable_type)
- expect(all('.label-link')[0].text).to have_content('B')
- expect(all('.label-link')[1].text).to have_content('X')
- expect(all('.label-link')[2].text).to have_content('a')
- expect(all('.label-link')[3].text).to have_content('z')
+ expect(all('.gl-label-text')[0].text).to have_content('B')
+ expect(all('.gl-label-text')[1].text).to have_content('X')
+ expect(all('.gl-label-text')[2].text).to have_content('a')
+ expect(all('.gl-label-text')[3].text).to have_content('z')
end
end
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index ee5773f1484..a518831ea2b 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -332,7 +332,7 @@ describe 'Filter issues', :js do
context 'issue label clicked' 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_tokens([label_token("\"#{multiple_words_label.title}\"")])
diff --git a/spec/features/labels_hierarchy_spec.rb b/spec/features/labels_hierarchy_spec.rb
index c1a2e22a0c2..c66d858a019 100644
--- a/spec/features/labels_hierarchy_spec.rb
+++ b/spec/features/labels_hierarchy_spec.rb
@@ -161,9 +161,9 @@ describe 'Labels Hierarchy', :js do
find('.btn-success').click
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.badge', 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: grandparent_group_label.title)
+ expect(page).to have_selector('span.gl-label-text', text: parent_group_label.title)
+ expect(page).to have_selector('span.gl-label-text', text: project_label_1.title)
end
end
diff --git a/spec/features/projects/container_registry_spec.rb b/spec/features/projects/container_registry_spec.rb
new file mode 100644
index 00000000000..02b2d03a880
--- /dev/null
+++ b/spec/features/projects/container_registry_spec.rb
@@ -0,0 +1,161 @@
+# 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
diff --git a/spec/frontend/lib/utils/unit_format/formatter_factory_spec.js b/spec/frontend/lib/utils/unit_format/formatter_factory_spec.js
new file mode 100644
index 00000000000..071ecde6a6d
--- /dev/null
+++ b/spec/frontend/lib/utils/unit_format/formatter_factory_spec.js
@@ -0,0 +1,200 @@
+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');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/unit_format/index_spec.js b/spec/frontend/lib/utils/unit_format/index_spec.js
new file mode 100644
index 00000000000..e0991f2909b
--- /dev/null
+++ b/spec/frontend/lib/utils/unit_format/index_spec.js
@@ -0,0 +1,117 @@
+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();
+ });
+ });
+});
diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb
index f5771405687..322390c3840 100644
--- a/spec/helpers/labels_helper_spec.rb
+++ b/spec/helpers/labels_helper_spec.rb
@@ -56,7 +56,7 @@ describe LabelsHelper do
context 'without subject' 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
@@ -65,7 +65,7 @@ describe LabelsHelper do
let(:subject) { build(:project, namespace: namespace, name: 'bar3') }
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
@@ -73,7 +73,7 @@ describe LabelsHelper do
let(:subject) { build(:group, name: 'bar') }
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
@@ -81,7 +81,7 @@ describe LabelsHelper do
['issue', :issue].each do |type|
context "set to #{type}" 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
@@ -89,7 +89,7 @@ describe LabelsHelper do
['merge_request', :merge_request].each do |type|
context "set to #{type}" 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
@@ -113,7 +113,7 @@ describe LabelsHelper do
context 'without block' do
it 'uses render_colored_label as the link content' do
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')
end
end
diff --git a/spec/helpers/markup_helper_spec.rb b/spec/helpers/markup_helper_spec.rb
index 3fb36e540b6..c22e20f0e73 100644
--- a/spec/helpers/markup_helper_spec.rb
+++ b/spec/helpers/markup_helper_spec.rb
@@ -537,8 +537,10 @@ describe MarkupHelper do
it 'does not style a label that can not be accessed by current_user' do
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
diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb
index 82df5064896..5a672de13d7 100644
--- a/spec/lib/banzai/filter/label_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb
@@ -28,7 +28,7 @@ describe Banzai::Filter::LabelReferenceFilter do
it 'includes default classes' do
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
it 'includes a data-project attribute' do
@@ -66,12 +66,12 @@ describe Banzai::Filter::LabelReferenceFilter do
describe 'label span element' do
it 'includes default classes' do
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
it 'includes a style attribute' do
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
@@ -85,7 +85,7 @@ describe Banzai::Filter::LabelReferenceFilter do
it 'links with adjacent text' do
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
it 'ignores invalid label IDs' do
@@ -109,7 +109,7 @@ describe Banzai::Filter::LabelReferenceFilter do
it 'links with adjacent text' do
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
it 'ignores invalid label names' do
@@ -133,7 +133,7 @@ describe Banzai::Filter::LabelReferenceFilter do
it 'links with adjacent text' do
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
it 'ignores invalid label names' do
@@ -158,7 +158,7 @@ describe Banzai::Filter::LabelReferenceFilter do
it 'does not include trailing punctuation', :aggregate_failures do
['.', ', ok?', '...', '?', '!', ': is that ok?'].each do |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
@@ -184,7 +184,7 @@ describe Banzai::Filter::LabelReferenceFilter do
it 'links with adjacent text' do
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
it 'ignores invalid label names' do
@@ -208,7 +208,7 @@ describe Banzai::Filter::LabelReferenceFilter do
it 'links with adjacent text' do
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
it 'ignores invalid label names' do
@@ -232,7 +232,7 @@ describe Banzai::Filter::LabelReferenceFilter do
it 'links with adjacent text' do
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
it 'ignores invalid label names' do
@@ -320,7 +320,7 @@ describe Banzai::Filter::LabelReferenceFilter do
it 'links with adjacent text' do
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
it 'includes a data-project attribute' do
@@ -358,7 +358,7 @@ describe Banzai::Filter::LabelReferenceFilter do
it 'links with adjacent text' do
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
it 'ignores invalid label names' do
@@ -381,7 +381,7 @@ describe Banzai::Filter::LabelReferenceFilter do
it 'links with adjacent text' do
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
it 'ignores invalid label names' do
diff --git a/spec/services/resource_events/change_milestone_service_spec.rb b/spec/services/resource_events/change_milestone_service_spec.rb
index c6b4f8e1b7e..bc634fadadd 100644
--- a/spec/services/resource_events/change_milestone_service_spec.rb
+++ b/spec/services/resource_events/change_milestone_service_spec.rb
@@ -3,65 +3,11 @@
require 'spec_helper'
describe ResourceEvents::ChangeMilestoneService do
- shared_examples 'milestone events creator' do
- let_it_be(:user) { create(:user) }
-
- 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 }
+ it_behaves_like 'a milestone events creator' do
+ let(:resource) { create(:issue) }
end
- it_behaves_like 'milestone events creator' do
- let(:resource) { merge_request }
+ it_behaves_like 'a milestone events creator' do
+ let(:resource) { create(:merge_request) }
end
end
diff --git a/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb b/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb
new file mode 100644
index 00000000000..77f64e5e8f8
--- /dev/null
+++ b/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb
@@ -0,0 +1,46 @@
+# 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