Commit e06d0e77 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent f7dae0cd
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;
},
......
......@@ -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;
},
},
});
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 '';
}
......
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}`;
}
......
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 '';
}
......
/**
* 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/
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,
......
<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>
......@@ -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 {
......
......@@ -51,7 +51,8 @@
}
.btn {
.spinner {
.spinner,
.gl-spinner {
vertical-align: text-bottom;
}
}
......@@ -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) {
......
......@@ -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;
}
......
......@@ -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;
}
}
}
......
......@@ -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;
}
}
}
/**
......
......@@ -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')
......@@ -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'
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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;
......
......@@ -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,
......
......@@ -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
......
......@@ -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|
......
......@@ -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
......
---
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
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')
......@@ -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)}")
......
......@@ -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)
......
......@@ -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 ""
......
......@@ -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
......
......@@ -4,30 +4,31 @@ require 'spec_helper'
describe 'Container Registry', :js do
let(:user) { create(:user) }
let(:project) { create(:project) }
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)
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'))
expect(page).to have_title _('Container Registry')
end
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
expect(page).to have_content 'no container images'
expect(page).to have_content _('There are no container images available in this group')
end
end
......@@ -37,39 +38,56 @@ describe 'Container Registry', :js do
project.container_repositories << container_repository
end
it 'user wants to see multi-level container repository' do
it 'list page has a list of images' do
visit_container_registry
expect(page).to have_content('my/image')
expect(page).to have_content 'my/image'
end
it 'user removes entire container repository', :sidekiq_might_not_need_inline do
it 'image repository delete is disabled' do
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(find('.modal .modal-title')).to have_content 'Remove repository'
find('.modal .modal-footer .btn-danger').click
expect(page).to have_content 'latest'
end
it 'user removes a specific tag from container repository' do
visit_container_registry
describe 'image repo details' do
before do
visit_container_registry_details 'my/image'
end
find('.js-toggle-repo').click
wait_for_requests
it 'shows the details breadcrumb' do
expect(find('.breadcrumbs')).to have_link 'my/image'
end
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 }
it 'shows the image title' do
expect(page).to have_content 'my/image tags'
end
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
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 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
......@@ -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
......
......@@ -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}\"")])
......
......@@ -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
......
# 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
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
......
......@@ -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
......
......@@ -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
......
......@@ -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
# 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