@@ -52,6 +52,7 @@ | |||
"@types/jest": "20.0.7", | |||
"@types/jquery": "3.2.11", | |||
"@types/lodash": "4.14.73", | |||
"@types/numeral": "0.0.22", | |||
"@types/prop-types": "15.5.1", | |||
"@types/react": "16.0.2", | |||
"@types/react-dom": "15.5.2", |
@@ -1,140 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import $ from 'jquery'; | |||
import { max } from 'd3-array'; | |||
import { select } from 'd3-selection'; | |||
import { scaleLinear, scaleBand } from 'd3-scale'; | |||
import { isSameDay, parseDate, toNotSoISOString } from '../../helpers/dates'; | |||
function trans(left, top) { | |||
return `translate(${left}, ${top})`; | |||
} | |||
const defaults = function() { | |||
return { | |||
height: 140, | |||
color: '#1f77b4', | |||
marginLeft: 1, | |||
marginRight: 1, | |||
marginTop: 18, | |||
marginBottom: 1 | |||
}; | |||
}; | |||
/* | |||
* data = [ | |||
* { val: '2015-01-30', count: 30 }, | |||
* ... | |||
* ] | |||
*/ | |||
$.fn.barchart = function(data) { | |||
$(this).each(function() { | |||
const options = { ...defaults(), ...$(this).data() }; | |||
Object.assign(options, { | |||
width: options.width || $(this).width(), | |||
endDate: options.endDate ? parseDate(options.endDate) : null | |||
}); | |||
const container = select(this); | |||
const svg = container | |||
.append('svg') | |||
.attr('width', options.width + 2) | |||
.attr('height', options.height + 2) | |||
.classed('sonar-d3', true); | |||
const plot = svg.append('g').classed('plot', true); | |||
const xScale = scaleBand().domain(data.map((d, i) => i)); | |||
const yScaleMax = max(data, d => d.count); | |||
const yScale = scaleLinear().domain([0, yScaleMax]); | |||
Object.assign(options, { | |||
availableWidth: options.width - options.marginLeft - options.marginRight, | |||
availableHeight: options.height - options.marginTop - options.marginBottom | |||
}); | |||
plot.attr('transform', trans(options.marginLeft, options.marginTop)); | |||
xScale.rangeRound([0, options.availableWidth]).paddingInner(0.05); | |||
yScale.range([3, options.availableHeight]); | |||
const barWidth = xScale.bandwidth(); | |||
const bars = plot.selectAll('g').data(data); | |||
if (barWidth > 0) { | |||
const barsEnter = bars | |||
.enter() | |||
.append('g') | |||
.attr('transform', (d, i) => | |||
trans(xScale(i), Math.ceil(options.availableHeight - yScale(d.count))) | |||
); | |||
barsEnter | |||
.append('rect') | |||
.style('fill', options.color) | |||
.attr('width', barWidth) | |||
.attr('height', d => Math.floor(yScale(d.count))) | |||
.style('cursor', 'pointer') | |||
.attr('data-period-start', d => toNotSoISOString(d.val)) | |||
.attr('data-period-end', (d, i) => { | |||
const ending = i < data.length - 1 ? data[i + 1].val : options.endDate; | |||
if (ending) { | |||
return toNotSoISOString(ending); | |||
} else { | |||
return ''; | |||
} | |||
}) | |||
.attr('title', (d, i) => { | |||
const beginning = parseDate(d.val); | |||
let ending = options.endDate; | |||
if (i < data.length - 1) { | |||
ending = parseDate(data[i + 1].val); | |||
ending.setDate(ending.getDate() - 1); | |||
} | |||
if (ending) { | |||
return ( | |||
d.text + | |||
'<br>' + | |||
beginning.format('LL') + | |||
(isSameDay(ending, beginning) ? '' : ' – ' + ending.format('LL')) | |||
); | |||
} else { | |||
return d.text + '<br>' + beginning.format('LL'); | |||
} | |||
}) | |||
.attr('data-placement', 'bottom') | |||
.attr('data-toggle', 'tooltip'); | |||
const maxValue = max(data, d => d.count); | |||
let isValueShown = false; | |||
barsEnter | |||
.append('text') | |||
.classed('subtitle', true) | |||
.attr('transform', trans(barWidth / 2, -4)) | |||
.style('text-anchor', 'middle') | |||
.text(d => { | |||
const text = !isValueShown && d.count === maxValue ? d.text : ''; | |||
isValueShown = d.count === maxValue; | |||
return text; | |||
}); | |||
$(this) | |||
.find('[data-toggle=tooltip]') | |||
.tooltip({ container: 'body', html: true }); | |||
} | |||
}); | |||
}; |
@@ -17,7 +17,6 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import { parseIssueFromResponse } from '../issues'; | |||
it('should populate comments data', () => { | |||
@@ -40,7 +39,7 @@ it('should populate comments data', () => { | |||
updatable: true | |||
} | |||
] | |||
}; | |||
} as any; | |||
expect(parseIssueFromResponse(issue, undefined, users, undefined).comments).toEqual([ | |||
{ | |||
author: 'admin', |
@@ -160,11 +160,11 @@ describe('#formatMeasure()', () => { | |||
}); | |||
it('should return null if value is empty string', () => { | |||
expect(formatMeasure('', 'PERCENT')).toBeNull(); | |||
expect(formatMeasure('', 'PERCENT')).toBe(''); | |||
}); | |||
it('should not fail without parameters', () => { | |||
expect(formatMeasure()).toBeNull(); | |||
it('should not fail with undefined', () => { | |||
expect(formatMeasure(undefined, 'INT')).toBe(''); | |||
}); | |||
}); | |||
@@ -252,7 +252,7 @@ describe('#formatMeasureVariation()', () => { | |||
expect(formatMeasureVariation('random value', 'RANDOM_TYPE')).toBe('random value'); | |||
}); | |||
it('should not fail without parameters', () => { | |||
expect(formatMeasureVariation()).toBeNull(); | |||
it('should not fail with undefined', () => { | |||
expect(formatMeasureVariation(undefined, 'INT')).toBe(''); | |||
}); | |||
}); |
@@ -42,7 +42,7 @@ describe('parseAsBoolean', () => { | |||
}); | |||
it('should return a default value', () => { | |||
expect(query.parseAsBoolean(1)).toBeTruthy(); | |||
expect(query.parseAsBoolean('1')).toBeTruthy(); | |||
expect(query.parseAsBoolean('foo')).toBeTruthy(); | |||
}); | |||
}); | |||
@@ -60,7 +60,7 @@ describe('parseAsString', () => { | |||
it('should parse strings correctly', () => { | |||
expect(query.parseAsString('random')).toBe('random'); | |||
expect(query.parseAsString('')).toBe(''); | |||
expect(query.parseAsString(null)).toBe(''); | |||
expect(query.parseAsString(undefined)).toBe(''); | |||
}); | |||
}); | |||
@@ -82,7 +82,6 @@ describe('serializeDate', () => { | |||
const date = parseDate('2016-06-20T13:09:48.256Z'); | |||
it('should serialize string correctly', () => { | |||
expect(query.serializeDate(date)).toBe('2016-06-20T13:09:48+0000'); | |||
expect(query.serializeDate('')).toBeUndefined(); | |||
expect(query.serializeDate()).toBeUndefined(); | |||
}); | |||
}); |
@@ -17,64 +17,71 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import { flatten, sortBy } from 'lodash'; | |||
import { SEVERITIES } from './constants'; | |||
/*:: import type { Issue, FlowLocation } from '../components/issue/types'; */ | |||
/*:: | |||
type TextRange = { | |||
startLine: number, | |||
endLine: number, | |||
startOffset: number, | |||
endOffset: number | |||
}; | |||
*/ | |||
/*:: | |||
type Comment = { | |||
login: string | |||
}; | |||
*/ | |||
/*:: | |||
type User = { | |||
login: string | |||
}; | |||
*/ | |||
/*:: | |||
type RawIssue = { | |||
assignee?: string, | |||
author: string, | |||
comments?: Array<Comment>, | |||
component: string, | |||
interface TextRange { | |||
startLine: number; | |||
endLine: number; | |||
startOffset: number; | |||
endOffset: number; | |||
} | |||
interface FlowLocation { | |||
msg: string; | |||
textRange?: TextRange; | |||
} | |||
interface Comment { | |||
login: string; | |||
[x: string]: any; | |||
} | |||
interface User { | |||
login: string; | |||
} | |||
interface Rule {} | |||
interface Component {} | |||
interface IssueBase { | |||
severity: string; | |||
[x: string]: any; | |||
} | |||
interface RawIssue extends IssueBase { | |||
assignee?: string; | |||
author?: string; | |||
comments?: Array<Comment>; | |||
component: string; | |||
flows?: Array<{ | |||
locations?: Array<{ msg: string, textRange?: TextRange }> | |||
}>, | |||
key: string, | |||
line?: number, | |||
project: string, | |||
rule: string, | |||
status: string, | |||
subProject?: string, | |||
textRange?: TextRange | |||
}; | |||
*/ | |||
export function sortBySeverity(issues /*: Array<*> */) { | |||
locations?: FlowLocation[]; | |||
}>; | |||
key: string; | |||
line?: number; | |||
project: string; | |||
rule: string; | |||
status: string; | |||
subProject?: string; | |||
textRange?: TextRange; | |||
} | |||
interface Issue extends IssueBase {} | |||
export function sortBySeverity(issues: Issue[]): Issue[] { | |||
return sortBy(issues, issue => SEVERITIES.indexOf(issue.severity)); | |||
} | |||
function injectRelational( | |||
issue /*: RawIssue | Comment */, | |||
source /*: ?Array<*> */, | |||
baseField /*: string */, | |||
lookupField /*: string */ | |||
issue: { [x: string]: any }, | |||
source: any[] | undefined, | |||
baseField: string, | |||
lookupField: string | |||
) { | |||
const newFields = {}; | |||
const newFields: { [x: string]: any } = {}; | |||
const baseValue = issue[baseField]; | |||
if (baseValue != null && source != null) { | |||
if (baseValue != undefined && source != undefined) { | |||
const lookupValue = source.find(candidate => candidate[lookupField] === baseValue); | |||
if (lookupValue != null) { | |||
Object.keys(lookupValue).forEach(key => { | |||
@@ -86,7 +93,7 @@ function injectRelational( | |||
return newFields; | |||
} | |||
function injectCommentsRelational(issue /*: RawIssue */, users /*: ?Array<User> */) { | |||
function injectCommentsRelational(issue: RawIssue, users?: User[]) { | |||
if (!issue.comments) { | |||
return {}; | |||
} | |||
@@ -100,11 +107,11 @@ function injectCommentsRelational(issue /*: RawIssue */, users /*: ?Array<User> | |||
return { comments }; | |||
} | |||
function prepareClosed(issue /*: RawIssue */) { | |||
function prepareClosed(issue: RawIssue): { flows?: undefined } { | |||
return issue.status === 'CLOSED' ? { flows: undefined } : {}; | |||
} | |||
function ensureTextRange(issue /*: RawIssue */) { | |||
function ensureTextRange(issue: RawIssue): { textRange?: TextRange } { | |||
return issue.line && !issue.textRange | |||
? { | |||
textRange: { | |||
@@ -117,20 +124,18 @@ function ensureTextRange(issue /*: RawIssue */) { | |||
: {}; | |||
} | |||
function reverseLocations(locations /*: Array<*> */) { | |||
function reverseLocations(locations: FlowLocation[]): FlowLocation[] { | |||
const x = [...locations]; | |||
x.reverse(); | |||
return x; | |||
} | |||
function splitFlows( | |||
issue /*: RawIssue */ | |||
// $FlowFixMe textRange is not null | |||
) /*: { secondaryLocations: Array<FlowLocation>, flows: Array<Array<FlowLocation>> } */ { | |||
issue: RawIssue | |||
): { secondaryLocations: Array<FlowLocation>; flows: Array<Array<FlowLocation>> } { | |||
const parsedFlows = (issue.flows || []) | |||
.filter(flow => flow.locations != null) | |||
// $FlowFixMe flow.locations is not null | |||
.map(flow => flow.locations.filter(location => location.textRange != null)); | |||
.map(flow => flow.locations!.filter(location => location.textRange != null)); | |||
const onlySecondaryLocations = parsedFlows.every(flow => flow.length === 1); | |||
@@ -140,11 +145,11 @@ function splitFlows( | |||
} | |||
export function parseIssueFromResponse( | |||
issue /*: Object */, | |||
components /*: ?Array<*> */, | |||
users /*: ?Array<*> */, | |||
rules /*: ?Array<*> */ | |||
) /*: Issue */ { | |||
issue: RawIssue, | |||
components?: Component[], | |||
users?: User[], | |||
rules?: Rule[] | |||
): Issue { | |||
const { secondaryLocations, flows } = splitFlows(issue); | |||
return { | |||
...issue, |
@@ -17,46 +17,55 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import numeral from 'numeral'; | |||
import * as numeral from 'numeral'; | |||
import { translate, translateWithParameters } from './l10n'; | |||
const HOURS_IN_DAY = 8; | |||
/** | |||
* Format a measure value for a given type | |||
* @param {string|number} value | |||
* @param {string} type | |||
*/ | |||
export function formatMeasure(value, type, options) { | |||
interface Measure { | |||
metric: string; | |||
periods?: any[]; | |||
} | |||
interface EnhancedMeasure { | |||
metric: Metric; | |||
} | |||
interface Metric { | |||
key: string; | |||
} | |||
interface Formatter { | |||
(value: string | number, options?: any): string; | |||
} | |||
/** Format a measure value for a given type */ | |||
export function formatMeasure( | |||
value: string | number | undefined, | |||
type: string, | |||
options?: any | |||
): string { | |||
const formatter = getFormatter(type); | |||
return useFormatter(value, formatter, options); | |||
} | |||
/** | |||
* Format a measure variation for a given type | |||
* @param {string|number} value | |||
* @param {string} type | |||
*/ | |||
export function formatMeasureVariation(value, type, options) { | |||
/** Format a measure variation for a given type */ | |||
export function formatMeasureVariation( | |||
value: string | number | undefined, | |||
type: string, | |||
options?: any | |||
): string { | |||
const formatter = getVariationFormatter(type); | |||
return useFormatter(value, formatter, options); | |||
} | |||
/** | |||
* Return a localized metric name | |||
* @param {string} metricKey | |||
* @returns {string} | |||
*/ | |||
export function localizeMetric(metricKey) { | |||
/** Return a localized metric name */ | |||
export function localizeMetric(metricKey: string): string { | |||
return translate('metric', metricKey, 'name'); | |||
} | |||
/** | |||
* Return corresponding "short" for better display in UI | |||
* @param {string} type | |||
* @returns {string} | |||
*/ | |||
export function getShortType(type) { | |||
/** Return corresponding "short" for better display in UI */ | |||
export function getShortType(type: string): string { | |||
if (type === 'INT') { | |||
return 'SHORT_INT'; | |||
} else if (type === 'WORK_DUR') { | |||
@@ -65,49 +74,38 @@ export function getShortType(type) { | |||
return type; | |||
} | |||
/** | |||
* Map metrics | |||
* @param {Array} measures | |||
* @param {Array} metrics | |||
* @returns {Array} | |||
*/ | |||
export function enhanceMeasuresWithMetrics(measures, metrics) { | |||
export function enhanceMeasuresWithMetrics( | |||
measures: Measure[], | |||
metrics: Metric[] | |||
): EnhancedMeasure[] { | |||
return measures.map(measure => { | |||
const metric = metrics.find(metric => metric.key === measure.metric); | |||
const metric = metrics.find(metric => metric.key === measure.metric) as Metric; | |||
return { ...measure, metric }; | |||
}); | |||
} | |||
/** | |||
* Get period value of a measure | |||
* @param measure | |||
* @param periodIndex | |||
*/ | |||
export function getPeriodValue(measure, periodIndex) { | |||
/** Get period value of a measure */ | |||
export function getPeriodValue(measure: Measure, periodIndex: number): string | number | undefined { | |||
const { periods } = measure; | |||
const period = periods.find(period => period.index === periodIndex); | |||
return period ? period.value : null; | |||
const period = periods && periods.find(period => period.index === periodIndex); | |||
return period ? period.value : undefined; | |||
} | |||
/** | |||
* Check if metric is differential | |||
* @param {string} metricKey | |||
* @returns {boolean} | |||
*/ | |||
export function isDiffMetric(metricKey) { | |||
/** Check if metric is differential */ | |||
export function isDiffMetric(metricKey: string): boolean { | |||
return metricKey.indexOf('new_') === 0; | |||
} | |||
/* | |||
* Helpers | |||
*/ | |||
function useFormatter(value, formatter, options) { | |||
return value != null && value !== '' && formatter != null ? formatter(value, options) : null; | |||
function useFormatter( | |||
value: string | number | undefined, | |||
formatter: Formatter, | |||
options?: any | |||
): string { | |||
return value != undefined && value !== '' ? formatter(value, options) : ''; | |||
} | |||
function getFormatter(type) { | |||
const FORMATTERS = { | |||
function getFormatter(type: string): Formatter { | |||
const FORMATTERS: { [type: string]: Formatter } = { | |||
INT: intFormatter, | |||
SHORT_INT: shortIntFormatter, | |||
FLOAT: floatFormatter, | |||
@@ -121,8 +119,8 @@ function getFormatter(type) { | |||
return FORMATTERS[type] || noFormatter; | |||
} | |||
function getVariationFormatter(type) { | |||
const FORMATTERS = { | |||
function getVariationFormatter(type: string): Formatter { | |||
const FORMATTERS: { [type: string]: Formatter } = { | |||
INT: intVariationFormatter, | |||
SHORT_INT: shortIntVariationFormatter, | |||
FLOAT: floatVariationFormatter, | |||
@@ -136,31 +134,27 @@ function getVariationFormatter(type) { | |||
return FORMATTERS[type] || noFormatter; | |||
} | |||
/* | |||
* Formatters | |||
*/ | |||
function genericFormatter(value, formatValue) { | |||
function genericNumberFormatter(value: number, formatValue?: string): string { | |||
return numeral(value).format(formatValue); | |||
} | |||
function noFormatter(value) { | |||
function noFormatter(value: string | number): string | number { | |||
return value; | |||
} | |||
function emptyFormatter() { | |||
return null; | |||
function emptyFormatter(): string { | |||
return ''; | |||
} | |||
function intFormatter(value) { | |||
return genericFormatter(value, '0,0'); | |||
function intFormatter(value: number): string { | |||
return genericNumberFormatter(value, '0,0'); | |||
} | |||
function intVariationFormatter(value) { | |||
return genericFormatter(value, '+0,0'); | |||
function intVariationFormatter(value: number): string { | |||
return genericNumberFormatter(value, '+0,0'); | |||
} | |||
function shortIntFormatter(value) { | |||
function shortIntFormatter(value: number): string { | |||
let format = '0,0'; | |||
if (value >= 1000) { | |||
format = '0.[0]a'; | |||
@@ -168,44 +162,53 @@ function shortIntFormatter(value) { | |||
if (value >= 10000) { | |||
format = '0a'; | |||
} | |||
return genericFormatter(value, format); | |||
return genericNumberFormatter(value, format); | |||
} | |||
function shortIntVariationFormatter(value) { | |||
function shortIntVariationFormatter(value: number): string { | |||
const formatted = shortIntFormatter(Math.abs(value)); | |||
return value < 0 ? `-${formatted}` : `+${formatted}`; | |||
} | |||
function floatFormatter(value) { | |||
return genericFormatter(value, '0,0.0[0000]'); | |||
function floatFormatter(value: number): string { | |||
return genericNumberFormatter(value, '0,0.0[0000]'); | |||
} | |||
function floatVariationFormatter(value) { | |||
return value === 0 ? '+0.0' : genericFormatter(value, '+0,0.0[0000]'); | |||
function floatVariationFormatter(value: number): string { | |||
return value === 0 ? '+0.0' : genericNumberFormatter(value, '+0,0.0[0000]'); | |||
} | |||
function percentFormatter(value, options = {}) { | |||
value = parseFloat(value); | |||
function percentFormatter(value: string | number, options: { decimals?: number } = {}): string { | |||
if (typeof value === 'string') { | |||
value = parseFloat(value); | |||
} | |||
if (options.decimals) { | |||
return genericFormatter(value / 100, `0,0.${'0'.repeat(options.decimals)}%`); | |||
return genericNumberFormatter(value / 100, `0,0.${'0'.repeat(options.decimals)}%`); | |||
} | |||
return value === 100 ? '100%' : genericFormatter(value / 100, '0,0.0%'); | |||
return value === 100 ? '100%' : genericNumberFormatter(value / 100, '0,0.0%'); | |||
} | |||
function percentVariationFormatter(value, options = {}) { | |||
value = parseFloat(value); | |||
function percentVariationFormatter( | |||
value: string | number, | |||
options: { decimals?: number } = {} | |||
): string { | |||
if (typeof value === 'string') { | |||
value = parseFloat(value); | |||
} | |||
if (options.decimals) { | |||
return genericFormatter(value / 100, `+0,0.${'0'.repeat(options.decimals)}%`); | |||
return genericNumberFormatter(value / 100, `+0,0.${'0'.repeat(options.decimals)}%`); | |||
} | |||
return value === 0 ? '+0.0%' : genericFormatter(value / 100, '+0,0.0%'); | |||
return value === 0 ? '+0.0%' : genericNumberFormatter(value / 100, '+0,0.0%'); | |||
} | |||
function ratingFormatter(value) { | |||
value = parseInt(value, 10); | |||
function ratingFormatter(value: string | number): string { | |||
if (typeof value === 'string') { | |||
value = parseInt(value, 10); | |||
} | |||
return String.fromCharCode(97 + value - 1).toUpperCase(); | |||
} | |||
function levelFormatter(value) { | |||
function levelFormatter(value: string): string { | |||
const l10nKey = 'metric.level.' + value; | |||
const result = translate(l10nKey); | |||
@@ -213,7 +216,7 @@ function levelFormatter(value) { | |||
return l10nKey !== result ? result : value; | |||
} | |||
function millisecondsFormatter(value) { | |||
function millisecondsFormatter(value: number): string { | |||
const ONE_SECOND = 1000; | |||
const ONE_MINUTE = 60 * ONE_SECOND; | |||
if (value >= ONE_MINUTE) { | |||
@@ -227,7 +230,7 @@ function millisecondsFormatter(value) { | |||
} | |||
} | |||
function millisecondsVariationFormatter(value) { | |||
function millisecondsVariationFormatter(value: number): string { | |||
const absValue = Math.abs(value); | |||
const formattedValue = millisecondsFormatter(absValue); | |||
return value < 0 ? `-${formattedValue}` : `+${formattedValue}`; | |||
@@ -237,31 +240,31 @@ function millisecondsVariationFormatter(value) { | |||
* Debt Formatters | |||
*/ | |||
function shouldDisplayDays(days) { | |||
function shouldDisplayDays(days: number): boolean { | |||
return days > 0; | |||
} | |||
function shouldDisplayDaysInShortFormat(days) { | |||
function shouldDisplayDaysInShortFormat(days: number): boolean { | |||
return days > 0.9; | |||
} | |||
function shouldDisplayHours(days, hours) { | |||
function shouldDisplayHours(days: number, hours: number): boolean { | |||
return hours > 0 && days < 10; | |||
} | |||
function shouldDisplayHoursInShortFormat(hours) { | |||
function shouldDisplayHoursInShortFormat(hours: number): boolean { | |||
return hours > 0.9; | |||
} | |||
function shouldDisplayMinutes(days, hours, minutes) { | |||
function shouldDisplayMinutes(days: number, hours: number, minutes: number): boolean { | |||
return minutes > 0 && hours < 10 && days === 0; | |||
} | |||
function addSpaceIfNeeded(value) { | |||
function addSpaceIfNeeded(value: string): string { | |||
return value.length > 0 ? value + ' ' : value; | |||
} | |||
function formatDuration(isNegative, days, hours, minutes) { | |||
function formatDuration(isNegative: boolean, days: number, hours: number, minutes: number): string { | |||
let formatted = ''; | |||
if (shouldDisplayDays(days)) { | |||
formatted += translateWithParameters('work_duration.x_days', isNegative ? -1 * days : days); | |||
@@ -283,7 +286,12 @@ function formatDuration(isNegative, days, hours, minutes) { | |||
return formatted; | |||
} | |||
function formatDurationShort(isNegative, days, hours, minutes) { | |||
function formatDurationShort( | |||
isNegative: boolean, | |||
days: number, | |||
hours: number, | |||
minutes: number | |||
): string { | |||
if (shouldDisplayDaysInShortFormat(days)) { | |||
const roundedDays = Math.round(days); | |||
const formattedDays = formatMeasure(isNegative ? -1 * roundedDays : roundedDays, 'SHORT_INT'); | |||
@@ -303,8 +311,11 @@ function formatDurationShort(isNegative, days, hours, minutes) { | |||
return translateWithParameters('work_duration.x_minutes', formattedMinutes); | |||
} | |||
function durationFormatter(value) { | |||
if (value === 0 || value === '0') { | |||
function durationFormatter(value: string | number): string { | |||
if (typeof value === 'string') { | |||
value = parseInt(value); | |||
} | |||
if (value === 0) { | |||
return '0'; | |||
} | |||
const hoursInDay = HOURS_IN_DAY; | |||
@@ -317,9 +328,11 @@ function durationFormatter(value) { | |||
return formatDuration(isNegative, days, hours, remainingValue); | |||
} | |||
function shortDurationFormatter(value) { | |||
value = parseInt(value, 10); | |||
if (value === 0 || value === '0') { | |||
function shortDurationFormatter(value: string | number): string { | |||
if (typeof value === 'string') { | |||
value = parseInt(value); | |||
} | |||
if (value === 0) { | |||
return '0'; | |||
} | |||
const hoursInDay = HOURS_IN_DAY; | |||
@@ -332,7 +345,7 @@ function shortDurationFormatter(value) { | |||
return formatDurationShort(isNegative, days, hours, remainingValue); | |||
} | |||
function durationVariationFormatter(value) { | |||
function durationVariationFormatter(value: string | number): string { | |||
if (value === 0 || value === '0') { | |||
return '+0'; | |||
} | |||
@@ -340,7 +353,7 @@ function durationVariationFormatter(value) { | |||
return formatted[0] !== '-' ? '+' + formatted : formatted; | |||
} | |||
function shortDurationVariationFormatter(value) { | |||
function shortDurationVariationFormatter(value: string | number): string { | |||
if (value === 0 || value === '0') { | |||
return '+0'; | |||
} | |||
@@ -348,7 +361,7 @@ function shortDurationVariationFormatter(value) { | |||
return formatted[0] !== '-' ? '+' + formatted : formatted; | |||
} | |||
function getRatingGrid() { | |||
function getRatingGrid(): string { | |||
// workaround cyclic dependencies | |||
const getStore = require('../app/utils/getStore').default; | |||
const { getGlobalSettingValue } = require('../store/rootReducer'); | |||
@@ -358,8 +371,8 @@ function getRatingGrid() { | |||
return settingValue ? settingValue.value : ''; | |||
} | |||
let maintainabilityRatingGrid; | |||
function getMaintainabilityRatingGrid() { | |||
let maintainabilityRatingGrid: number[]; | |||
function getMaintainabilityRatingGrid(): number[] { | |||
if (maintainabilityRatingGrid) { | |||
return maintainabilityRatingGrid; | |||
} | |||
@@ -379,7 +392,7 @@ function getMaintainabilityRatingGrid() { | |||
return maintainabilityRatingGrid; | |||
} | |||
function getMaintainabilityRatingTooltip(rating) { | |||
function getMaintainabilityRatingTooltip(rating: number): string { | |||
const maintainabilityGrid = getMaintainabilityRatingGrid(); | |||
const maintainabilityRatingThreshold = maintainabilityGrid[Math.floor(rating) - 2]; | |||
@@ -399,7 +412,7 @@ function getMaintainabilityRatingTooltip(rating) { | |||
); | |||
} | |||
export function getRatingTooltip(metricKey, value) { | |||
export function getRatingTooltip(metricKey: string, value: number): string { | |||
const ratingLetter = formatMeasure(value, 'RATING'); | |||
const finalMetricKey = metricKey.startsWith('new_') ? metricKey.substr(4) : metricKey; |
@@ -20,21 +20,29 @@ | |||
import { translate, translateWithParameters } from './l10n'; | |||
import { parseDate } from './dates'; | |||
export function getPeriod(periods, index) { | |||
interface Period { | |||
date: string; | |||
index: number; | |||
mode: string; | |||
modeParam?: string; | |||
parameter: string; | |||
} | |||
export function getPeriod(periods: Period[] | undefined, index: number): Period | undefined { | |||
if (!Array.isArray(periods)) { | |||
return null; | |||
return undefined; | |||
} | |||
return periods.find(period => period.index === index); | |||
} | |||
export function getLeakPeriod(periods) { | |||
export function getLeakPeriod(periods: Period[] | undefined): Period | undefined { | |||
return getPeriod(periods, 1); | |||
} | |||
export function getPeriodLabel(period) { | |||
export function getPeriodLabel(period: Period | undefined): string | undefined { | |||
if (!period) { | |||
return null; | |||
return undefined; | |||
} | |||
const parameter = period.modeParam || period.parameter; | |||
@@ -46,14 +54,10 @@ export function getPeriodLabel(period) { | |||
return translateWithParameters(`overview.period.${period.mode}`, parameter); | |||
} | |||
export function getPeriodDate(period) { | |||
if (!period) { | |||
return null; | |||
} | |||
return parseDate(period.date); | |||
export function getPeriodDate(period: Period | undefined): Date | undefined { | |||
return period ? parseDate(period.date) : undefined; | |||
} | |||
export function getLeakPeriodLabel(periods) { | |||
export function getLeakPeriodLabel(periods: Period[]): string | undefined { | |||
return getPeriodLabel(getLeakPeriod(periods)); | |||
} |
@@ -17,15 +17,14 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import { isNil, omitBy } from 'lodash'; | |||
import { isValidDate, parseDate, toNotSoISOString } from './dates'; | |||
/*:: | |||
export type RawQuery = { [string]: any }; | |||
*/ | |||
export interface RawQuery { | |||
[x: string]: any; | |||
} | |||
function arraysEqual(a, b) { | |||
function arraysEqual(a: RawQuery, b: RawQuery): boolean { | |||
if (a.length !== b.length) { | |||
return false; | |||
} | |||
@@ -37,7 +36,7 @@ function arraysEqual(a, b) { | |||
return true; | |||
} | |||
export function queriesEqual(a /*: Object */, b /*: Object */) /*: boolean */ { | |||
export function queriesEqual(a: RawQuery, b: RawQuery): boolean { | |||
const keysA = Object.keys(a); | |||
const keysB = Object.keys(b); | |||
@@ -53,48 +52,50 @@ export function queriesEqual(a /*: Object */, b /*: Object */) /*: boolean */ { | |||
); | |||
} | |||
export function cleanQuery(query /*: { [string]: ?string } */) /*: RawQuery */ { | |||
export function cleanQuery(query: RawQuery): RawQuery { | |||
return omitBy(query, isNil); | |||
} | |||
export function parseAsBoolean( | |||
value /*: ?string */, | |||
defaultValue /*: boolean */ = true | |||
) /*: boolean */ { | |||
export function parseAsBoolean(value: string | undefined, defaultValue: boolean = true): boolean { | |||
return value === 'false' ? false : value === 'true' ? true : defaultValue; | |||
} | |||
export function parseAsDate(value /*: ?string */) /*: Date | void */ { | |||
export function parseAsDate(value?: string): Date | undefined { | |||
if (value) { | |||
const date = parseDate(value); | |||
if (isValidDate(date)) { | |||
return date; | |||
} | |||
} | |||
return undefined; | |||
} | |||
export function parseAsFacetMode(facetMode /*: string */) { | |||
export function parseAsFacetMode(facetMode: string): string { | |||
return facetMode === 'debt' || facetMode === 'effort' ? 'effort' : 'count'; | |||
} | |||
export function parseAsString(value /*: ?string */) /*: string */ { | |||
export function parseAsString(value: string | undefined): string { | |||
return value || ''; | |||
} | |||
export function parseAsArray(value /*: ?string */, itemParser /*: string => * */) /*: Array<*> */ { | |||
export function parseAsArray( | |||
value: string | undefined, | |||
itemParser: (x: string) => string | |||
): string[] { | |||
return value ? value.split(',').map(itemParser) : []; | |||
} | |||
export function serializeDate(value /*: ?Date */) /*: string | void */ { | |||
export function serializeDate(value?: Date): string | undefined { | |||
if (value != null && value.toISOString) { | |||
return toNotSoISOString(value); | |||
} | |||
return undefined; | |||
} | |||
export function serializeString(value /*: string */) /*: ?string */ { | |||
export function serializeString(value: string): string | undefined { | |||
return value || undefined; | |||
} | |||
export function serializeStringArray(value /*: ?Array<string> */) /*: ?string */ { | |||
export function serializeStringArray(value: string[] | undefined[]): string | undefined { | |||
return value && value.length ? value.join() : undefined; | |||
} |
@@ -54,6 +54,10 @@ | |||
version "8.0.22" | |||
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.22.tgz#9c6bfee1f45f5e9952ff6b487e657ecca48c7777" | |||
"@types/numeral@0.0.22": | |||
version "0.0.22" | |||
resolved "https://registry.yarnpkg.com/@types/numeral/-/numeral-0.0.22.tgz#86bef1f0a2d743afdc2ef3168d45f2905e1a0b93" | |||
"@types/prop-types@15.5.1": | |||
version "15.5.1" | |||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.5.1.tgz#1ecf52621299e65b855374337fb11fd2d1066fc1" |