diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-25 18:08:50 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-25 18:08:50 +0300 |
commit | e06d0e779673d745972863302858105aad9032e5 (patch) | |
tree | 0ff35b27a949a164f586613004b4abfe33e7d20e /app | |
parent | f7dae0cdcb70ecb71c1d65f099e9d96b27a4548c (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
26 files changed, 506 insertions, 96 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? - 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? - 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 } × 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 |