* Create a query helper library and use it in issues page and project activity pagetags/6.5-M2
@@ -30,30 +30,15 @@ type GetProjectActivityResponse = { | |||
}; | |||
type GetProjectActivityOptions = { | |||
project: string, | |||
category?: ?string, | |||
pageIndex?: ?number, | |||
pageSize?: ?number | |||
p?: ?number, | |||
ps?: ?number | |||
}; | |||
export const getProjectActivity = ( | |||
project: string, | |||
options?: GetProjectActivityOptions | |||
): Promise<GetProjectActivityResponse> => { | |||
const data: Object = { project }; | |||
if (options) { | |||
if (options.category) { | |||
data.category = options.category; | |||
} | |||
if (options.pageIndex) { | |||
data.p = options.pageIndex; | |||
} | |||
if (options.pageSize) { | |||
data.ps = options.pageSize; | |||
} | |||
} | |||
return getJSON('/api/project_analyses/search', data); | |||
}; | |||
data: GetProjectActivityOptions | |||
): Promise<GetProjectActivityResponse> => getJSON('/api/project_analyses/search', data); | |||
type CreateEventResponse = { | |||
analysis: string, |
@@ -88,7 +88,8 @@ export default class MeasureHistory extends React.PureComponent { | |||
return Promise.resolve([]); | |||
} | |||
return getProjectActivity(this.props.component.key, { | |||
return getProjectActivity({ | |||
project: this.props.component.key, | |||
category: 'VERSION' | |||
}).then(({ analyses }) => { | |||
const events = analyses.map(analysis => { |
@@ -58,15 +58,19 @@ import EmptySearch from '../../../components/common/EmptySearch'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { scrollToElement } from '../../../helpers/scrolling'; | |||
import type { Issue } from '../../../components/issue/types'; | |||
import type { RawQuery } from '../../../helpers/query'; | |||
import '../styles.css'; | |||
export type Props = { | |||
component?: Component, | |||
currentUser: CurrentUser, | |||
fetchIssues: ({}) => Promise<*>, | |||
location: { pathname: string, query: { [string]: string } }, | |||
fetchIssues: (query: RawQuery) => Promise<*>, | |||
location: { pathname: string, query: RawQuery }, | |||
onRequestFail: Error => void, | |||
router: { push: ({}) => void, replace: ({}) => void } | |||
router: { | |||
push: ({ pathname: string, query?: RawQuery }) => void, | |||
replace: ({ pathname: string, query?: RawQuery }) => void | |||
} | |||
}; | |||
export type State = { |
@@ -29,8 +29,7 @@ import { getOrganizations } from '../../../api/organizations'; | |||
import { receiveOrganizations } from '../../../store/organizations/duck'; | |||
import { searchIssues } from '../../../api/issues'; | |||
import { parseIssueFromResponse } from '../../../helpers/issues'; | |||
type Query = { [string]: string }; | |||
import type { RawQuery } from '../../../helpers/query'; | |||
const mapStateToProps = (state, ownProps) => ({ | |||
component: ownProps.location.query.id | |||
@@ -51,7 +50,7 @@ const fetchIssueOrganizations = issues => dispatch => { | |||
); | |||
}; | |||
const fetchIssues = (query: Query) => dispatch => | |||
const fetchIssues = (query: RawQuery) => dispatch => | |||
searchIssues({ ...query, additionalFields: '_all' }) | |||
.then(response => { | |||
const parsedIssues = response.issues.map(issue => |
@@ -19,7 +19,7 @@ | |||
*/ | |||
// @flow | |||
import { parseQuery, areMyIssuesSelected, serializeQuery } from './utils'; | |||
import type { RawQuery } from './utils'; | |||
import type { RawQuery } from '../../helpers/query'; | |||
const parseHash = (hash: string): RawQuery => { | |||
const query: RawQuery = {}; |
@@ -18,11 +18,19 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import { isNil, omitBy } from 'lodash'; | |||
import { searchMembers } from '../../api/organizations'; | |||
import { searchUsers } from '../../api/users'; | |||
export type RawQuery = { [string]: string }; | |||
import { | |||
queriesEqual, | |||
cleanQuery, | |||
parseAsBoolean, | |||
parseAsFacetMode, | |||
parseAsArray, | |||
parseAsString, | |||
serializeString, | |||
serializeStringArray | |||
} from '../../helpers/query'; | |||
import type { RawQuery } from '../../helpers/query'; | |||
export type Query = {| | |||
assigned: boolean, | |||
@@ -56,112 +64,70 @@ export type Paging = { | |||
total: number | |||
}; | |||
const parseAsBoolean = (value: ?string, defaultValue: boolean = true): boolean => | |||
(value === 'false' ? false : value === 'true' ? true : defaultValue); | |||
const parseAsString = (value: ?string): string => value || ''; | |||
const parseAsStringArray = (value: ?string): Array<string> => (value ? value.split(',') : []); | |||
const parseAsFacetMode = (facetMode: string) => | |||
(facetMode === 'debt' || facetMode === 'effort' ? 'effort' : 'count'); | |||
// allow sorting by CREATION_DATE only | |||
const parseAsSort = (sort: string): string => (sort === 'CREATION_DATE' ? 'CREATION_DATE' : ''); | |||
export const parseQuery = (query: RawQuery): Query => ({ | |||
assigned: parseAsBoolean(query.assigned), | |||
assignees: parseAsStringArray(query.assignees), | |||
authors: parseAsStringArray(query.authors), | |||
assignees: parseAsArray(query.assignees, parseAsString), | |||
authors: parseAsArray(query.authors, parseAsString), | |||
createdAfter: parseAsString(query.createdAfter), | |||
createdAt: parseAsString(query.createdAt), | |||
createdBefore: parseAsString(query.createdBefore), | |||
createdInLast: parseAsString(query.createdInLast), | |||
directories: parseAsStringArray(query.directories), | |||
directories: parseAsArray(query.directories, parseAsString), | |||
facetMode: parseAsFacetMode(query.facetMode), | |||
files: parseAsStringArray(query.fileUuids), | |||
issues: parseAsStringArray(query.issues), | |||
languages: parseAsStringArray(query.languages), | |||
modules: parseAsStringArray(query.moduleUuids), | |||
projects: parseAsStringArray(query.projectUuids), | |||
files: parseAsArray(query.fileUuids, parseAsString), | |||
issues: parseAsArray(query.issues, parseAsString), | |||
languages: parseAsArray(query.languages, parseAsString), | |||
modules: parseAsArray(query.moduleUuids, parseAsString), | |||
projects: parseAsArray(query.projectUuids, parseAsString), | |||
resolved: parseAsBoolean(query.resolved), | |||
resolutions: parseAsStringArray(query.resolutions), | |||
rules: parseAsStringArray(query.rules), | |||
resolutions: parseAsArray(query.resolutions, parseAsString), | |||
rules: parseAsArray(query.rules, parseAsString), | |||
sort: parseAsSort(query.s), | |||
severities: parseAsStringArray(query.severities), | |||
severities: parseAsArray(query.severities, parseAsString), | |||
sinceLeakPeriod: parseAsBoolean(query.sinceLeakPeriod, false), | |||
statuses: parseAsStringArray(query.statuses), | |||
tags: parseAsStringArray(query.tags), | |||
types: parseAsStringArray(query.types) | |||
statuses: parseAsArray(query.statuses, parseAsString), | |||
tags: parseAsArray(query.tags, parseAsString), | |||
types: parseAsArray(query.types, parseAsString) | |||
}); | |||
export const getOpen = (query: RawQuery) => query.open; | |||
export const areMyIssuesSelected = (query: RawQuery): boolean => query.myIssues === 'true'; | |||
const serializeString = (value: string): ?string => value || undefined; | |||
const serializeValue = (value: Array<string>): ?string => (value.length ? value.join() : undefined); | |||
export const serializeQuery = (query: Query): RawQuery => { | |||
const filter = { | |||
assigned: query.assigned ? undefined : 'false', | |||
assignees: serializeValue(query.assignees), | |||
authors: serializeValue(query.authors), | |||
assignees: serializeStringArray(query.assignees), | |||
authors: serializeStringArray(query.authors), | |||
createdAfter: serializeString(query.createdAfter), | |||
createdAt: serializeString(query.createdAt), | |||
createdBefore: serializeString(query.createdBefore), | |||
createdInLast: serializeString(query.createdInLast), | |||
directories: serializeValue(query.directories), | |||
directories: serializeStringArray(query.directories), | |||
facetMode: query.facetMode === 'effort' ? serializeString(query.facetMode) : undefined, | |||
fileUuids: serializeValue(query.files), | |||
issues: serializeValue(query.issues), | |||
languages: serializeValue(query.languages), | |||
moduleUuids: serializeValue(query.modules), | |||
projectUuids: serializeValue(query.projects), | |||
fileUuids: serializeStringArray(query.files), | |||
issues: serializeStringArray(query.issues), | |||
languages: serializeStringArray(query.languages), | |||
moduleUuids: serializeStringArray(query.modules), | |||
projectUuids: serializeStringArray(query.projects), | |||
resolved: query.resolved ? undefined : 'false', | |||
resolutions: serializeValue(query.resolutions), | |||
resolutions: serializeStringArray(query.resolutions), | |||
s: serializeString(query.sort), | |||
severities: serializeValue(query.severities), | |||
severities: serializeStringArray(query.severities), | |||
sinceLeakPeriod: query.sinceLeakPeriod ? 'true' : undefined, | |||
statuses: serializeValue(query.statuses), | |||
rules: serializeValue(query.rules), | |||
tags: serializeValue(query.tags), | |||
types: serializeValue(query.types) | |||
statuses: serializeStringArray(query.statuses), | |||
rules: serializeStringArray(query.rules), | |||
tags: serializeStringArray(query.tags), | |||
types: serializeStringArray(query.types) | |||
}; | |||
return omitBy(filter, isNil); | |||
}; | |||
const areArraysEqual = (a: Array<string>, b: Array<string>) => { | |||
if (a.length !== b.length) { | |||
return false; | |||
} | |||
for (let i = 0; i < a.length; i++) { | |||
if (a[i] !== b[i]) { | |||
return false; | |||
} | |||
} | |||
return true; | |||
return cleanQuery(filter); | |||
}; | |||
export const areQueriesEqual = (a: RawQuery, b: RawQuery) => { | |||
const parsedA: Query = parseQuery(a); | |||
const parsedB: Query = parseQuery(b); | |||
const keysA = Object.keys(parsedA); | |||
const keysB = Object.keys(parsedB); | |||
if (keysA.length !== keysB.length) { | |||
return false; | |||
} | |||
return keysA.every( | |||
key => | |||
(Array.isArray(parsedA[key]) && Array.isArray(parsedB[key]) | |||
? areArraysEqual(parsedA[key], parsedB[key]) | |||
: parsedA[key] === parsedB[key]) | |||
); | |||
}; | |||
export const areQueriesEqual = (a: RawQuery, b: RawQuery) => | |||
queriesEqual(parseQuery(a), parseQuery(b)); | |||
type RawFacet = { | |||
property: string, |
@@ -20,26 +20,27 @@ | |||
// @flow | |||
import React from 'react'; | |||
import { Link } from 'react-router'; | |||
import { connect } from 'react-redux'; | |||
import Analysis from './Analysis'; | |||
import throwGlobalError from '../../../app/utils/throwGlobalError'; | |||
import { getProjectActivity } from '../../../api/projectActivity'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { fetchRecentProjectActivity } from '../actions'; | |||
import { getProjectActivity } from '../../../store/rootReducer'; | |||
import { getAnalyses } from '../../../store/projectActivity/duck'; | |||
import type { Analysis as AnalysisType } from '../../projectActivity/types'; | |||
type Props = { | |||
analyses?: Array<*>, | |||
project: string, | |||
fetchRecentProjectActivity: (project: string) => Promise<*> | |||
project: string | |||
}; | |||
class AnalysesList extends React.PureComponent { | |||
type State = { | |||
analyses: Array<AnalysisType>, | |||
loading: boolean | |||
}; | |||
const PAGE_SIZE = 5; | |||
export default class AnalysesList extends React.PureComponent { | |||
mounted: boolean; | |||
props: Props; | |||
state = { | |||
loading: true | |||
}; | |||
state: State = { analyses: [], loading: true }; | |||
componentDidMount() { | |||
this.mounted = true; | |||
@@ -58,14 +59,16 @@ class AnalysesList extends React.PureComponent { | |||
fetchData() { | |||
this.setState({ loading: true }); | |||
this.props.fetchRecentProjectActivity(this.props.project).then(() => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
} | |||
}); | |||
getProjectActivity({ project: this.props.project, ps: PAGE_SIZE }) | |||
.then(({ analyses }) => { | |||
if (this.mounted) { | |||
this.setState({ analyses, loading: false }); | |||
} | |||
}) | |||
.catch(throwGlobalError); | |||
} | |||
renderList(analyses) { | |||
renderList(analyses: Array<AnalysisType>) { | |||
if (!analyses.length) { | |||
return ( | |||
<p className="spacer-top note"> | |||
@@ -82,10 +85,9 @@ class AnalysesList extends React.PureComponent { | |||
} | |||
render() { | |||
const { analyses } = this.props; | |||
const { loading } = this.state; | |||
const { analyses, loading } = this.state; | |||
if (loading || !analyses) { | |||
if (loading) { | |||
return null; | |||
} | |||
@@ -106,11 +108,3 @@ class AnalysesList extends React.PureComponent { | |||
); | |||
} | |||
} | |||
const mapStateToProps = (state, ownProps: Props) => ({ | |||
analyses: getAnalyses(getProjectActivity(state), ownProps.project) | |||
}); | |||
const mapDispatchToProps = { fetchRecentProjectActivity }; | |||
export default connect(mapStateToProps, mapDispatchToProps)(AnalysesList); |
@@ -22,7 +22,7 @@ import Events from '../../projectActivity/components/Events'; | |||
import FormattedDate from '../../../components/ui/FormattedDate'; | |||
import { TooltipsContainer } from '../../../components/mixins/tooltips-mixin'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import type { Analysis as AnalysisType } from '../../../store/projectActivity/duck'; | |||
import type { Analysis as AnalysisType } from '../../projectActivity/types'; | |||
export default function Analysis(props: { analysis: AnalysisType }) { | |||
const { analysis } = props; |
@@ -0,0 +1,85 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`addCustomEvent should correctly add a custom event 1`] = ` | |||
Object { | |||
"date": "2016-10-27T12:21:15+0200", | |||
"events": Array [ | |||
Object { | |||
"category": "Custom", | |||
"key": "Enew", | |||
"name": "Foo", | |||
}, | |||
], | |||
"key": "A2", | |||
} | |||
`; | |||
exports[`changeEvent should correctly update an event 1`] = ` | |||
Object { | |||
"date": "2016-10-27T16:33:50+0200", | |||
"events": Array [ | |||
Object { | |||
"category": "VERSION", | |||
"key": "E1", | |||
"name": "changed", | |||
}, | |||
], | |||
"key": "A1", | |||
} | |||
`; | |||
exports[`deleteAnalysis should correctly delete an analyses 1`] = ` | |||
Array [ | |||
Object { | |||
"date": "2016-10-27T12:21:15+0200", | |||
"events": Array [], | |||
"key": "A2", | |||
}, | |||
Object { | |||
"date": "2016-10-26T12:17:29+0200", | |||
"events": Array [ | |||
Object { | |||
"category": "OTHER", | |||
"key": "E2", | |||
"name": "foo", | |||
}, | |||
Object { | |||
"category": "OTHER", | |||
"key": "E3", | |||
"name": "foo", | |||
}, | |||
], | |||
"key": "A3", | |||
}, | |||
] | |||
`; | |||
exports[`deleteEvent should correctly remove an event 1`] = ` | |||
Object { | |||
"date": "2016-10-27T16:33:50+0200", | |||
"events": Array [], | |||
"key": "A1", | |||
} | |||
`; | |||
exports[`deleteEvent should correctly remove an event 2`] = ` | |||
Object { | |||
"date": "2016-10-27T12:21:15+0200", | |||
"events": Array [], | |||
"key": "A2", | |||
} | |||
`; | |||
exports[`deleteEvent should correctly remove an event 3`] = ` | |||
Object { | |||
"date": "2016-10-26T12:17:29+0200", | |||
"events": Array [ | |||
Object { | |||
"category": "OTHER", | |||
"key": "E3", | |||
"name": "foo", | |||
}, | |||
], | |||
"key": "A3", | |||
} | |||
`; |
@@ -0,0 +1,106 @@ | |||
/* | |||
* 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 * as actions from '../actions'; | |||
const ANALYSES = [ | |||
{ | |||
key: 'A1', | |||
date: '2016-10-27T16:33:50+0200', | |||
events: [ | |||
{ | |||
key: 'E1', | |||
category: 'VERSION', | |||
name: '6.5-SNAPSHOT' | |||
} | |||
] | |||
}, | |||
{ | |||
key: 'A2', | |||
date: '2016-10-27T12:21:15+0200', | |||
events: [] | |||
}, | |||
{ | |||
key: 'A3', | |||
date: '2016-10-26T12:17:29+0200', | |||
events: [ | |||
{ | |||
key: 'E2', | |||
category: 'OTHER', | |||
name: 'foo' | |||
}, | |||
{ | |||
key: 'E3', | |||
category: 'OTHER', | |||
name: 'foo' | |||
} | |||
] | |||
} | |||
]; | |||
const newEvent = { | |||
key: 'Enew', | |||
name: 'Foo', | |||
category: 'Custom' | |||
}; | |||
it('should never throw when there is no analyses', () => { | |||
expect(actions.addCustomEvent('A1', newEvent)({})).toBeUndefined(); | |||
expect(actions.deleteEvent('A1', newEvent)({})).toBeUndefined(); | |||
expect(actions.changeEvent('A1', newEvent)({})).toBeUndefined(); | |||
expect(actions.deleteAnalysis('Anew')({})).toBeUndefined(); | |||
}); | |||
describe('addCustomEvent', () => { | |||
it('should correctly add a custom event', () => { | |||
expect( | |||
actions.addCustomEvent('A2', newEvent)({ analyses: ANALYSES }).analyses[1] | |||
).toMatchSnapshot(); | |||
expect( | |||
actions.addCustomEvent('A1', newEvent)({ analyses: ANALYSES }).analyses[0].events | |||
).toContain(newEvent); | |||
}); | |||
}); | |||
describe('deleteEvent', () => { | |||
it('should correctly remove an event', () => { | |||
expect(actions.deleteEvent('A1', 'E1')({ analyses: ANALYSES }).analyses[0]).toMatchSnapshot(); | |||
expect(actions.deleteEvent('A2', 'E1')({ analyses: ANALYSES }).analyses[1]).toMatchSnapshot(); | |||
expect(actions.deleteEvent('A3', 'E2')({ analyses: ANALYSES }).analyses[2]).toMatchSnapshot(); | |||
}); | |||
}); | |||
describe('changeEvent', () => { | |||
it('should correctly update an event', () => { | |||
expect( | |||
actions.changeEvent('A1', { key: 'E1', name: 'changed' })({ analyses: ANALYSES }).analyses[0] | |||
).toMatchSnapshot(); | |||
expect( | |||
actions.changeEvent('A2', { key: 'E2' })({ analyses: ANALYSES }).analyses[1].events | |||
).toHaveLength(0); | |||
}); | |||
}); | |||
describe('deleteAnalysis', () => { | |||
it('should correctly delete an analyses', () => { | |||
expect(actions.deleteAnalysis('A1')({ analyses: ANALYSES }).analyses).toMatchSnapshot(); | |||
expect(actions.deleteAnalysis('A5')({ analyses: ANALYSES }).analyses).toHaveLength(3); | |||
expect(actions.deleteAnalysis('A2')({ analyses: ANALYSES }).analyses).toHaveLength(2); | |||
}); | |||
}); |
@@ -18,81 +18,44 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import * as api from '../../api/projectActivity'; | |||
import { | |||
receiveProjectActivity, | |||
addEvent, | |||
deleteEvent as deleteEventAction, | |||
changeEvent as changeEventAction, | |||
deleteAnalysis as deleteAnalysisAction, | |||
getPaging | |||
} from '../../store/projectActivity/duck'; | |||
import { onFail } from '../../store/rootActions'; | |||
import { getProjectActivity } from '../../store/rootReducer'; | |||
const rejectOnFail = (dispatch: Function) => (error: Object) => { | |||
onFail(dispatch)(error); | |||
return Promise.reject(); | |||
}; | |||
export const fetchProjectActivity = (project: string, filter: ?string) => ( | |||
dispatch: Function | |||
): void => { | |||
api | |||
.getProjectActivity(project, { category: filter }) | |||
.then( | |||
({ analyses, paging }) => dispatch(receiveProjectActivity(project, analyses, paging)), | |||
onFail(dispatch) | |||
); | |||
}; | |||
export const fetchMoreProjectActivity = (project: string, filter: ?string) => ( | |||
dispatch: Function, | |||
getState: Function | |||
): void => { | |||
const projectActivity = getProjectActivity(getState()); | |||
const { pageIndex } = getPaging(projectActivity, project); | |||
api | |||
.getProjectActivity(project, { category: filter, pageIndex: pageIndex + 1 }) | |||
.then( | |||
({ analyses, paging }) => dispatch(receiveProjectActivity(project, analyses, paging)), | |||
onFail(dispatch) | |||
); | |||
}; | |||
export const addCustomEvent = (analysis: string, name: string, category?: string) => ( | |||
dispatch: Function | |||
): Promise<*> => { | |||
return api | |||
.createEvent(analysis, name, category) | |||
.then(({ analysis, ...event }) => dispatch(addEvent(analysis, event)), rejectOnFail(dispatch)); | |||
}; | |||
export const deleteEvent = (analysis: string, event: string) => ( | |||
dispatch: Function | |||
): Promise<*> => { | |||
return api | |||
.deleteEvent(event) | |||
.then(() => dispatch(deleteEventAction(analysis, event)), rejectOnFail(dispatch)); | |||
}; | |||
export const addVersion = (analysis: string, version: string) => ( | |||
dispatch: Function | |||
): Promise<*> => { | |||
return dispatch(addCustomEvent(analysis, version, 'VERSION')); | |||
}; | |||
export const changeEvent = (event: string, name: string) => (dispatch: Function): Promise<*> => { | |||
return api | |||
.changeEvent(event, name) | |||
.then(() => dispatch(changeEventAction(event, { name })), rejectOnFail(dispatch)); | |||
}; | |||
export const deleteAnalysis = (project: string, analysis: string) => ( | |||
dispatch: Function | |||
): Promise<*> => { | |||
return api | |||
.deleteAnalysis(analysis) | |||
.then(() => dispatch(deleteAnalysisAction(project, analysis)), rejectOnFail(dispatch)); | |||
}; | |||
import type { Event } from './types'; | |||
import type { State } from './components/ProjectActivityApp'; | |||
export const addCustomEvent = (analysis: string, event: Event) => (state: State) => ({ | |||
analyses: state.analyses.map(item => { | |||
if (item.key !== analysis) { | |||
return item; | |||
} | |||
return { ...item, events: [...item.events, event] }; | |||
}) | |||
}); | |||
export const deleteEvent = (analysis: string, event: string) => (state: State) => ({ | |||
analyses: state.analyses.map(item => { | |||
if (item.key !== analysis) { | |||
return item; | |||
} | |||
return { | |||
...item, | |||
events: item.events.filter(eventItem => eventItem.key !== event) | |||
}; | |||
}) | |||
}); | |||
export const changeEvent = (analysis: string, event: Event) => (state: State) => ({ | |||
analyses: state.analyses.map(item => { | |||
if (item.key !== analysis) { | |||
return item; | |||
} | |||
return { | |||
...item, | |||
events: item.events.map( | |||
eventItem => (eventItem.key === event.key ? { ...eventItem, ...event } : eventItem) | |||
) | |||
}; | |||
}) | |||
}); | |||
export const deleteAnalysis = (analysis: string) => (state: State) => ({ | |||
analyses: state.analyses.filter(item => item.key !== analysis) | |||
}); |
@@ -20,17 +20,19 @@ | |||
// @flow | |||
import React from 'react'; | |||
import EventInner from './EventInner'; | |||
import ChangeCustomEventForm from './forms/ChangeCustomEventForm'; | |||
import RemoveCustomEventForm from './forms/RemoveCustomEventForm'; | |||
import ChangeEventForm from './forms/ChangeEventForm'; | |||
import RemoveEventForm from './forms/RemoveEventForm'; | |||
import DeleteIcon from './DeleteIcon'; | |||
import ChangeIcon from './ChangeIcon'; | |||
import type { Event as EventType } from '../../../store/projectActivity/duck'; | |||
import type { Event as EventType } from '../types'; | |||
type Props = { | |||
analysis: string, | |||
canAdmin: boolean, | |||
changeEvent: (event: string, name: string) => Promise<*>, | |||
deleteEvent: (analysis: string, event: string) => Promise<*>, | |||
event: EventType, | |||
isFirst: boolean, | |||
canAdmin: boolean | |||
isFirst: boolean | |||
}; | |||
type State = { | |||
@@ -41,7 +43,6 @@ type State = { | |||
export default class Event extends React.PureComponent { | |||
mounted: boolean; | |||
props: Props; | |||
state: State = { | |||
changing: false, | |||
deleting: false | |||
@@ -77,9 +78,10 @@ export default class Event extends React.PureComponent { | |||
render() { | |||
const { event, canAdmin } = this.props; | |||
const canChange = ['OTHER', 'VERSION'].includes(event.category); | |||
const canDelete = | |||
event.category === 'OTHER' || (event.category === 'VERSION' && !this.props.isFirst); | |||
const isOther = event.category === 'OTHER'; | |||
const isVersion = !isOther && event.category === 'VERSION'; | |||
const canChange = isOther || isVersion; | |||
const canDelete = isOther || (isVersion && !this.props.isFirst); | |||
const showActions = canAdmin && (canChange || canDelete); | |||
return ( | |||
@@ -99,13 +101,29 @@ export default class Event extends React.PureComponent { | |||
</div>} | |||
{this.state.changing && | |||
<ChangeCustomEventForm event={this.props.event} onClose={this.stopChanging} />} | |||
<ChangeEventForm | |||
changeEventButtonText={ | |||
'project_activity.' + (isVersion ? 'change_version' : 'change_custom_event') | |||
} | |||
changeEvent={this.props.changeEvent} | |||
event={this.props.event} | |||
onClose={this.stopChanging} | |||
/>} | |||
{this.state.deleting && | |||
<RemoveCustomEventForm | |||
<RemoveEventForm | |||
analysis={this.props.analysis} | |||
deleteEvent={this.props.deleteEvent} | |||
event={this.props.event} | |||
onClose={this.stopDeleting} | |||
removeEventButtonText={ | |||
'project_activity.' + (isVersion ? 'remove_version' : 'remove_custom_event') | |||
} | |||
removeEventQuestion={ | |||
'project_activity.' + | |||
(isVersion ? 'remove_version' : 'remove_custom_event') + | |||
'.question' | |||
} | |||
/>} | |||
</div> | |||
); |
@@ -20,7 +20,7 @@ | |||
// @flow | |||
import React from 'react'; | |||
import { TooltipsContainer } from '../../../components/mixins/tooltips-mixin'; | |||
import type { Event as EventType } from '../../../store/projectActivity/duck'; | |||
import type { Event as EventType } from '../types'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import './Event.css'; | |||
@@ -21,13 +21,15 @@ | |||
import React from 'react'; | |||
import { sortBy } from 'lodash'; | |||
import Event from './Event'; | |||
import type { Event as EventType } from '../../../store/projectActivity/duck'; | |||
import type { Event as EventType } from '../types'; | |||
type Props = { | |||
analysis: string, | |||
canAdmin: boolean, | |||
changeEvent: (event: string, name: string) => Promise<*>, | |||
deleteEvent: (analysis: string, event: string) => Promise<*>, | |||
events: Array<EventType>, | |||
isFirst: boolean, | |||
canAdmin: boolean | |||
isFirst: boolean | |||
}; | |||
export default function Events(props: Props) { | |||
@@ -43,11 +45,13 @@ export default function Events(props: Props) { | |||
<div className="project-activity-events"> | |||
{sortedEvents.map(event => ( | |||
<Event | |||
key={event.key} | |||
analysis={props.analysis} | |||
canAdmin={props.canAdmin} | |||
changeEvent={props.changeEvent} | |||
deleteEvent={props.deleteEvent} | |||
event={event} | |||
isFirst={props.isFirst} | |||
canAdmin={props.canAdmin} | |||
key={event.key} | |||
/> | |||
))} | |||
</div> |
@@ -19,27 +19,24 @@ | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import { groupBy } from 'lodash'; | |||
import moment from 'moment'; | |||
import ProjectActivityAnalysis from './ProjectActivityAnalysis'; | |||
import FormattedDate from '../../../components/ui/FormattedDate'; | |||
import { getProjectActivity } from '../../../store/rootReducer'; | |||
import { getAnalyses } from '../../../store/projectActivity/duck'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import type { Analysis } from '../../../store/projectActivity/duck'; | |||
import type { Analysis } from '../types'; | |||
type Props = { | |||
project: string, | |||
analyses?: Array<Analysis>, | |||
canAdmin: boolean | |||
addCustomEvent: (analysis: string, name: string, category?: string) => Promise<*>, | |||
addVersion: (analysis: string, version: string) => Promise<*>, | |||
analyses: Array<Analysis>, | |||
canAdmin: boolean, | |||
changeEvent: (event: string, name: string) => Promise<*>, | |||
deleteAnalysis: (analysis: string) => Promise<*>, | |||
deleteEvent: (analysis: string, event: string) => Promise<*> | |||
}; | |||
function ProjectActivityAnalysesList(props: Props) { | |||
if (!props.analyses) { | |||
return null; | |||
} | |||
export default function ProjectActivityAnalysesList(props: Props) { | |||
if (props.analyses.length === 0) { | |||
return <div className="note">{translate('no_results')}</div>; | |||
} | |||
@@ -64,11 +61,15 @@ function ProjectActivityAnalysesList(props: Props) { | |||
{byDay[day] != null && | |||
byDay[day].map(analysis => ( | |||
<ProjectActivityAnalysis | |||
key={analysis.key} | |||
addCustomEvent={props.addCustomEvent} | |||
addVersion={props.addVersion} | |||
analysis={analysis} | |||
isFirst={analysis === firstAnalysis} | |||
project={props.project} | |||
canAdmin={props.canAdmin} | |||
changeEvent={props.changeEvent} | |||
deleteAnalysis={props.deleteAnalysis} | |||
deleteEvent={props.deleteEvent} | |||
isFirst={analysis === firstAnalysis} | |||
key={analysis.key} | |||
/> | |||
))} | |||
</ul> | |||
@@ -78,9 +79,3 @@ function ProjectActivityAnalysesList(props: Props) { | |||
</div> | |||
); | |||
} | |||
const mapStateToProps = (state, ownProps) => ({ | |||
analyses: getAnalyses(getProjectActivity(state), ownProps.project) | |||
}); | |||
export default connect(mapStateToProps)(ProjectActivityAnalysesList); |
@@ -20,17 +20,20 @@ | |||
// @flow | |||
import React from 'react'; | |||
import Events from './Events'; | |||
import AddVersionForm from './forms/AddVersionForm'; | |||
import AddCustomEventForm from './forms/AddCustomEventForm'; | |||
import AddEventForm from './forms/AddEventForm'; | |||
import RemoveAnalysisForm from './forms/RemoveAnalysisForm'; | |||
import FormattedDate from '../../../components/ui/FormattedDate'; | |||
import type { Analysis } from '../../../store/projectActivity/duck'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import type { Analysis } from '../types'; | |||
type Props = { | |||
addCustomEvent: (analysis: string, name: string, category?: string) => Promise<*>, | |||
addVersion: (analysis: string, version: string) => Promise<*>, | |||
analysis: Analysis, | |||
changeEvent: (event: string, name: string) => Promise<*>, | |||
deleteAnalysis: (analysis: string) => Promise<*>, | |||
deleteEvent: (analysis: string, event: string) => Promise<*>, | |||
isFirst: boolean, | |||
project: string, | |||
canAdmin: boolean | |||
}; | |||
@@ -51,17 +54,25 @@ export default function ProjectActivityAnalysis(props: Props) { | |||
<ul className="dropdown-menu dropdown-menu-right"> | |||
{version == null && | |||
<li> | |||
<AddVersionForm analysis={props.analysis} /> | |||
<AddEventForm | |||
addEvent={props.addVersion} | |||
analysis={props.analysis} | |||
addEventButtonText="project_activity.add_version" | |||
/> | |||
</li>} | |||
<li> | |||
<AddCustomEventForm analysis={props.analysis} /> | |||
<AddEventForm | |||
addEvent={props.addCustomEvent} | |||
analysis={props.analysis} | |||
addEventButtonText="project_activity.add_custom_event" | |||
/> | |||
</li> | |||
</ul> | |||
</div> | |||
{!isFirst && | |||
<div className="display-inline-block little-spacer-left"> | |||
<RemoveAnalysisForm analysis={props.analysis} project={props.project} /> | |||
<RemoveAnalysisForm analysis={props.analysis} deleteAnalysis={props.deleteAnalysis} /> | |||
</div>} | |||
</div>} | |||
@@ -72,9 +83,11 @@ export default function ProjectActivityAnalysis(props: Props) { | |||
{events.length > 0 && | |||
<Events | |||
analysis={props.analysis.key} | |||
canAdmin={canAdmin} | |||
changeEvent={props.changeEvent} | |||
deleteEvent={props.deleteEvent} | |||
events={events} | |||
isFirst={props.isFirst} | |||
canAdmin={canAdmin} | |||
/>} | |||
</li> | |||
); |
@@ -20,54 +20,151 @@ | |||
// @flow | |||
import React from 'react'; | |||
import Helmet from 'react-helmet'; | |||
import { connect } from 'react-redux'; | |||
import ProjectActivityPageHeader from './ProjectActivityPageHeader'; | |||
import ProjectActivityAnalysesList from './ProjectActivityAnalysesList'; | |||
import ProjectActivityPageFooter from './ProjectActivityPageFooter'; | |||
import { fetchProjectActivity } from '../actions'; | |||
import { getComponent } from '../../../store/rootReducer'; | |||
import throwGlobalError from '../../../app/utils/throwGlobalError'; | |||
import * as api from '../../../api/projectActivity'; | |||
import * as actions from '../actions'; | |||
import { parseQuery, serializeQuery, serializeUrlQuery } from '../utils'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import './projectActivity.css'; | |||
import type { Analysis, Query, Paging } from '../types'; | |||
import type { RawQuery } from '../../../helpers/query'; | |||
type Props = { | |||
location: { query: { id: string } }, | |||
fetchProjectActivity: (project: string, filter: ?string) => void, | |||
project: { configuration?: { showHistory: boolean } } | |||
location: { pathname: string, query: RawQuery }, | |||
project: { configuration?: { showHistory: boolean }, key: string }, | |||
router: { push: ({ pathname: string, query?: RawQuery }) => void } | |||
}; | |||
type State = { | |||
filter: ?string | |||
export type State = { | |||
analyses: Array<Analysis>, | |||
loading: boolean, | |||
paging?: Paging, | |||
query: Query | |||
}; | |||
class ProjectActivityApp extends React.PureComponent { | |||
export default class ProjectActivityApp extends React.PureComponent { | |||
mounted: boolean; | |||
props: Props; | |||
state: State; | |||
state: State = { | |||
filter: null | |||
}; | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { analyses: [], loading: true, query: parseQuery(props.location.query) }; | |||
} | |||
componentDidMount() { | |||
const html = document.querySelector('html'); | |||
if (html) { | |||
html.classList.add('dashboard-page'); | |||
this.mounted = true; | |||
this.handleQueryChange(); | |||
const elem = document.querySelector('html'); | |||
elem && elem.classList.add('dashboard-page'); | |||
} | |||
componentDidUpdate(prevProps: Props) { | |||
if (prevProps.location.query !== this.props.location.query) { | |||
this.handleQueryChange(); | |||
} | |||
this.props.fetchProjectActivity(this.props.location.query.id); | |||
} | |||
componentWillUnmount() { | |||
const html = document.querySelector('html'); | |||
if (html) { | |||
html.classList.remove('dashboard-page'); | |||
this.mounted = false; | |||
const elem = document.querySelector('html'); | |||
elem && elem.classList.remove('dashboard-page'); | |||
} | |||
fetchActivity = ( | |||
query: Query, | |||
additional?: {} | |||
): Promise<{ analyses: Array<Analysis>, paging: Paging }> => { | |||
const parameters = { | |||
...serializeQuery(query), | |||
...additional | |||
}; | |||
return api.getProjectActivity(parameters).catch(throwGlobalError); | |||
}; | |||
fetchMoreActivity = () => { | |||
const { paging, query } = this.state; | |||
if (!paging) { | |||
return; | |||
} | |||
this.setState({ loading: true }); | |||
this.fetchActivity(query, { p: paging.pageIndex + 1 }).then(({ analyses, paging }) => { | |||
if (this.mounted) { | |||
this.setState((state: State) => ({ | |||
analyses: state.analyses ? state.analyses.concat(analyses) : analyses, | |||
loading: false, | |||
paging | |||
})); | |||
} | |||
}); | |||
}; | |||
addCustomEvent = (analysis: string, name: string, category?: string): Promise<*> => | |||
api | |||
.createEvent(analysis, name, category) | |||
.then( | |||
({ analysis, ...event }) => | |||
this.mounted && this.setState(actions.addCustomEvent(analysis, event)) | |||
) | |||
.catch(throwGlobalError); | |||
addVersion = (analysis: string, version: string): Promise<*> => | |||
this.addCustomEvent(analysis, version, 'VERSION'); | |||
deleteEvent = (analysis: string, event: string): Promise<*> => | |||
api | |||
.deleteEvent(event) | |||
.then(() => this.mounted && this.setState(actions.deleteEvent(analysis, event))) | |||
.catch(throwGlobalError); | |||
changeEvent = (event: string, name: string): Promise<*> => | |||
api | |||
.changeEvent(event, name) | |||
.then( | |||
({ analysis, ...event }) => | |||
this.mounted && this.setState(actions.changeEvent(analysis, event)) | |||
) | |||
.catch(throwGlobalError); | |||
deleteAnalysis = (analysis: string): Promise<*> => | |||
api | |||
.deleteAnalysis(analysis) | |||
.then(() => this.mounted && this.setState(actions.deleteAnalysis(analysis))) | |||
.catch(throwGlobalError); | |||
handleQueryChange() { | |||
const query = parseQuery(this.props.location.query); | |||
this.setState({ loading: true, query }); | |||
this.fetchActivity(query).then(({ analyses, paging }) => { | |||
if (this.mounted) { | |||
this.setState({ | |||
analyses, | |||
loading: false, | |||
paging | |||
}); | |||
} | |||
}); | |||
} | |||
handleFilter = (filter: ?string) => { | |||
this.setState({ filter }); | |||
this.props.fetchProjectActivity(this.props.location.query.id, filter); | |||
updateQuery = (newQuery: Query) => { | |||
this.props.router.push({ | |||
pathname: this.props.location.pathname, | |||
query: { | |||
...serializeUrlQuery({ | |||
...this.state.query, | |||
...newQuery | |||
}), | |||
id: this.props.project.key | |||
} | |||
}); | |||
}; | |||
render() { | |||
const project = this.props.location.query.id; | |||
const { query } = this.state; | |||
const { configuration } = this.props.project; | |||
const canAdmin = configuration ? configuration.showHistory : false; | |||
@@ -75,24 +172,24 @@ class ProjectActivityApp extends React.PureComponent { | |||
<div id="project-activity" className="page page-limited"> | |||
<Helmet title={translate('project_activity.page')} /> | |||
<ProjectActivityPageHeader | |||
project={project} | |||
filter={this.state.filter} | |||
changeFilter={this.handleFilter} | |||
/> | |||
<ProjectActivityPageHeader category={query.category} updateQuery={this.updateQuery} /> | |||
<ProjectActivityAnalysesList project={project} canAdmin={canAdmin} /> | |||
<ProjectActivityAnalysesList | |||
addCustomEvent={this.addCustomEvent} | |||
addVersion={this.addVersion} | |||
analyses={this.state.analyses} | |||
canAdmin={canAdmin} | |||
changeEvent={this.changeEvent} | |||
deleteAnalysis={this.deleteAnalysis} | |||
deleteEvent={this.deleteEvent} | |||
/> | |||
<ProjectActivityPageFooter project={project} /> | |||
<ProjectActivityPageFooter | |||
analyses={this.state.analyses} | |||
fetchMoreActivity={this.fetchMoreActivity} | |||
paging={this.state.paging} | |||
/> | |||
</div> | |||
); | |||
} | |||
} | |||
const mapStateToProps = (state, ownProps: Props) => ({ | |||
project: getComponent(state, ownProps.location.query.id) | |||
}); | |||
const mapDispatchToProps = { fetchProjectActivity }; | |||
export default connect(mapStateToProps, mapDispatchToProps)(ProjectActivityApp); |
@@ -18,16 +18,13 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import * as api from '../../api/projectActivity'; | |||
import { receiveProjectActivity } from '../../store/projectActivity/duck'; | |||
import { onFail } from '../../store/rootActions'; | |||
import { connect } from 'react-redux'; | |||
import { withRouter } from 'react-router'; | |||
import ProjectActivityApp from './ProjectActivityApp'; | |||
import { getComponent } from '../../../store/rootReducer'; | |||
const PAGE_SIZE = 5; | |||
const mapStateToProps = (state, ownProps) => ({ | |||
project: getComponent(state, ownProps.location.query.id) | |||
}); | |||
export const fetchRecentProjectActivity = (project: string) => (dispatch: Function) => | |||
api | |||
.getProjectActivity(project, { pageSize: PAGE_SIZE }) | |||
.then( | |||
({ analyses, paging }) => dispatch(receiveProjectActivity(project, analyses, paging)), | |||
onFail(dispatch) | |||
); | |||
export default connect(mapStateToProps)(withRouter(ProjectActivityApp)); |
@@ -19,43 +19,18 @@ | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import ListFooter from '../../../components/controls/ListFooter'; | |||
import { getProjectActivity } from '../../../store/rootReducer'; | |||
import { getAnalyses, getPaging } from '../../../store/projectActivity/duck'; | |||
import { fetchMoreProjectActivity } from '../actions'; | |||
import type { Paging } from '../../../store/projectActivity/duck'; | |||
import type { Paging } from '../types'; | |||
class ProjectActivityPageFooter extends React.PureComponent { | |||
props: { | |||
analyses: Array<*>, | |||
paging: ?Paging, | |||
project: string, | |||
fetchMoreProjectActivity: (project: string) => void | |||
}; | |||
type Props = { | |||
analyses: Array<*>, | |||
fetchMoreActivity: () => void, | |||
paging?: Paging | |||
}; | |||
handleLoadMore = () => { | |||
this.props.fetchMoreProjectActivity(this.props.project); | |||
}; | |||
render() { | |||
const { analyses, paging } = this.props; | |||
if (!paging || analyses.length === 0) { | |||
return null; | |||
} | |||
return ( | |||
<ListFooter count={analyses.length} total={paging.total} loadMore={this.handleLoadMore} /> | |||
); | |||
export default function ProjectActivityPageFooter({ analyses, fetchMoreActivity, paging }: Props) { | |||
if (!paging || analyses.length === 0) { | |||
return null; | |||
} | |||
return <ListFooter count={analyses.length} total={paging.total} loadMore={fetchMoreActivity} />; | |||
} | |||
const mapStateToProps = (state, ownProps) => ({ | |||
analyses: getAnalyses(getProjectActivity(state), ownProps.project), | |||
paging: getPaging(getProjectActivity(state), ownProps.project) | |||
}); | |||
const mapDispatchToProps = { fetchMoreProjectActivity }; | |||
export default connect(mapStateToProps, mapDispatchToProps)(ProjectActivityPageFooter); |
@@ -21,18 +21,20 @@ | |||
import React from 'react'; | |||
import Select from 'react-select'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import type { RawQuery } from '../../../helpers/query'; | |||
type Props = { | |||
changeFilter: (filter: ?string) => void, | |||
filter: ?string | |||
updateQuery: RawQuery => void, | |||
category?: string | |||
}; | |||
export default class ProjectActivityPageHeader extends React.PureComponent { | |||
props: Props; | |||
handleChange = (option: null | { value: string }) => { | |||
this.props.changeFilter(option && option.value); | |||
handleCategoryChange = (option: ?{ value: string }) => { | |||
this.props.updateQuery({ category: option ? option.value : '' }); | |||
}; | |||
render() { | |||
const selectOptions = ['VERSION', 'QUALITY_GATE', 'QUALITY_PROFILE', 'OTHER'].map(category => ({ | |||
label: translate('event.category', category), | |||
@@ -47,9 +49,9 @@ export default class ProjectActivityPageHeader extends React.PureComponent { | |||
placeholder={translate('filter_verb') + '...'} | |||
clearable={true} | |||
searchable={false} | |||
value={this.props.filter} | |||
value={this.props.category} | |||
options={selectOptions} | |||
onChange={this.handleChange} | |||
onChange={this.handleCategoryChange} | |||
/> | |||
</div> | |||
@@ -1,40 +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. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import { addCustomEvent } from '../../actions'; | |||
import AddEventForm from './AddEventForm'; | |||
import type { Analysis } from '../../../../store/projectActivity/duck'; | |||
type Props = { | |||
addEvent: (analysis: string, name: string, category?: string) => Promise<*>, | |||
analysis: Analysis | |||
}; | |||
function AddCustomEventForm(props: Props) { | |||
return <AddEventForm {...props} addEventButtonText="project_activity.add_custom_event" />; | |||
} | |||
const mapStateToProps = null; | |||
const mapDispatchToProps = { addEvent: addCustomEvent }; | |||
export default connect(mapStateToProps, mapDispatchToProps)(AddCustomEventForm); |
@@ -20,8 +20,8 @@ | |||
// @flow | |||
import React from 'react'; | |||
import Modal from 'react-modal'; | |||
import type { Analysis } from '../../../../store/projectActivity/duck'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
import type { Analysis } from '../../types'; | |||
type Props = { | |||
addEvent: (analysis: string, name: string, category?: string) => Promise<*>, | |||
@@ -38,7 +38,6 @@ type State = { | |||
export default class AddEventForm extends React.PureComponent { | |||
mounted: boolean; | |||
props: Props; | |||
state: State = { | |||
open: false, | |||
processing: false, | |||
@@ -131,7 +130,6 @@ export default class AddEventForm extends React.PureComponent { | |||
</div>} | |||
</footer> | |||
</form> | |||
</Modal> | |||
); | |||
} |
@@ -1,40 +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. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import { addVersion } from '../../actions'; | |||
import AddEventForm from './AddEventForm'; | |||
import type { Analysis } from '../../../../store/projectActivity/duck'; | |||
type Props = { | |||
addEvent: (analysis: string, version: string) => Promise<*>, | |||
analysis: Analysis | |||
}; | |||
function AddVersionForm(props: Props) { | |||
return <AddEventForm {...props} addEventButtonText="project_activity.add_version" />; | |||
} | |||
const mapStateToProps = null; | |||
const mapDispatchToProps = { addEvent: addVersion }; | |||
export default connect(mapStateToProps, mapDispatchToProps)(AddVersionForm); |
@@ -1,41 +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. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import ChangeEventForm from './ChangeEventForm'; | |||
import { changeEvent } from '../../actions'; | |||
import type { Event } from '../../../../store/projectActivity/duck'; | |||
type Props = { | |||
changeEvent: (event: string, name: string) => Promise<*>, | |||
event: Event, | |||
onClose: () => void | |||
}; | |||
const ChangeCustomEventForm = (props: Props) => ( | |||
<ChangeEventForm {...props} changeEventButtonText="project_activity.change_custom_event" /> | |||
); | |||
const mapStateToProps = null; | |||
const mapDispatchToProps = { changeEvent }; | |||
export default connect(mapStateToProps, mapDispatchToProps)(ChangeCustomEventForm); |
@@ -20,8 +20,8 @@ | |||
// @flow | |||
import React from 'react'; | |||
import Modal from 'react-modal'; | |||
import type { Event } from '../../../../store/projectActivity/duck'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
import type { Event } from '../../types'; | |||
type Props = { | |||
changeEvent: (event: string, name: string) => Promise<*>, | |||
@@ -129,7 +129,6 @@ export default class ChangeEventForm extends React.PureComponent { | |||
</div>} | |||
</footer> | |||
</form> | |||
</Modal> | |||
); | |||
} |
@@ -19,16 +19,13 @@ | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import Modal from 'react-modal'; | |||
import type { Analysis } from '../../../../store/projectActivity/duck'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
import { deleteAnalysis } from '../../actions'; | |||
import type { Analysis } from '../../types'; | |||
type Props = { | |||
analysis: Analysis, | |||
deleteAnalysis: (project: string, analysis: string) => Promise<*>, | |||
project: string | |||
deleteAnalysis: (analysis: string) => Promise<*> | |||
}; | |||
type State = { | |||
@@ -36,10 +33,9 @@ type State = { | |||
processing: boolean | |||
}; | |||
class RemoveAnalysisForm extends React.PureComponent { | |||
export default class RemoveAnalysisForm extends React.PureComponent { | |||
mounted: boolean; | |||
props: Props; | |||
state: State = { | |||
open: false, | |||
processing: false | |||
@@ -81,7 +77,7 @@ class RemoveAnalysisForm extends React.PureComponent { | |||
e.preventDefault(); | |||
this.setState({ processing: true }); | |||
this.props | |||
.deleteAnalysis(this.props.project, this.props.analysis.key) | |||
.deleteAnalysis(this.props.analysis.key) | |||
.then(this.stopProcessingAndClose, this.stopProcessing); | |||
}; | |||
@@ -128,9 +124,3 @@ class RemoveAnalysisForm extends React.PureComponent { | |||
); | |||
} | |||
} | |||
const mapStateToProps = null; | |||
const mapDispatchToProps = { deleteAnalysis }; | |||
export default connect(mapStateToProps, mapDispatchToProps)(RemoveAnalysisForm); |
@@ -1,48 +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. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import RemoveEventForm from './RemoveEventForm'; | |||
import { deleteEvent } from '../../actions'; | |||
import type { Event } from '../../../../store/projectActivity/duck'; | |||
type Props = { | |||
analysis: string, | |||
event: Event, | |||
deleteEvent: (analysis: string, event: string) => Promise<*>, | |||
onClose: () => void | |||
}; | |||
function RemoveCustomEventForm(props: Props) { | |||
return ( | |||
<RemoveEventForm | |||
{...props} | |||
removeEventButtonText="project_activity.remove_custom_event" | |||
removeEventQuestion="project_activity.remove_custom_event.question" | |||
/> | |||
); | |||
} | |||
const mapStateToProps = null; | |||
const mapDispatchToProps = { deleteEvent }; | |||
export default connect(mapStateToProps, mapDispatchToProps)(RemoveCustomEventForm); |
@@ -20,8 +20,8 @@ | |||
// @flow | |||
import React from 'react'; | |||
import Modal from 'react-modal'; | |||
import type { Event } from '../../../../store/projectActivity/duck'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
import type { Event } from '../../types'; | |||
type Props = { | |||
analysis: string, | |||
@@ -39,7 +39,6 @@ type State = { | |||
export default class RemoveEventForm extends React.PureComponent { | |||
mounted: boolean; | |||
props: Props; | |||
state: State = { | |||
processing: false | |||
}; | |||
@@ -108,7 +107,6 @@ export default class RemoveEventForm extends React.PureComponent { | |||
</div>} | |||
</footer> | |||
</form> | |||
</Modal> | |||
); | |||
} |
@@ -1,48 +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. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import RemoveEventForm from './RemoveEventForm'; | |||
import { deleteEvent } from '../../actions'; | |||
import type { Event } from '../../../../store/projectActivity/duck'; | |||
type Props = { | |||
analysis: string, | |||
event: Event, | |||
deleteEvent: (analysis: string, event: string) => Promise<*>, | |||
onClose: () => void | |||
}; | |||
function RemoveVersionForm(props: Props) { | |||
return ( | |||
<RemoveEventForm | |||
{...props} | |||
removeEventButtonText="project_activity.remove_version" | |||
removeEventQuestion="project_activity.remove_version.question" | |||
/> | |||
); | |||
} | |||
const mapStateToProps = null; | |||
const mapDispatchToProps = { deleteEvent }; | |||
export default connect(mapStateToProps, mapDispatchToProps)(RemoveVersionForm); |
@@ -21,7 +21,7 @@ const routes = [ | |||
{ | |||
getIndexRoute(_, callback) { | |||
require.ensure([], require => | |||
callback(null, { component: require('./components/ProjectActivityApp').default }) | |||
callback(null, { component: require('./components/ProjectActivityAppContainer').default }) | |||
); | |||
} | |||
} |
@@ -18,16 +18,27 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import type { Paging, ReceiveProjectActivityAction } from './duck'; | |||
export type State = { | |||
[key: string]: Paging | |||
export type Event = { | |||
key: string, | |||
name: string, | |||
category: string, | |||
description?: string | |||
}; | |||
export default (state: State = {}, action: ReceiveProjectActivityAction): State => { | |||
if (action.type === 'RECEIVE_PROJECT_ACTIVITY') { | |||
return { ...state, [action.project]: action.paging }; | |||
} | |||
export type Analysis = { | |||
key: string, | |||
date: string, | |||
events: Array<Event> | |||
}; | |||
export type Paging = { | |||
pageIndex: number, | |||
pageSize: number, | |||
total: number | |||
}; | |||
return state; | |||
export type Query = { | |||
project: string, | |||
category: string | |||
}; |
@@ -18,24 +18,23 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import ChangeEventForm from './ChangeEventForm'; | |||
import { changeEvent } from '../../actions'; | |||
import type { Event } from '../../../../store/projectActivity/duck'; | |||
import { cleanQuery, parseAsString, serializeString } from '../../helpers/query'; | |||
import type { Query } from './types'; | |||
import type { RawQuery } from '../../helpers/query'; | |||
type Props = { | |||
changeEvent: (event: string, name: string) => Promise<*>, | |||
event: Event, | |||
onClose: () => void | |||
}; | |||
export const parseQuery = (urlQuery: RawQuery): Query => ({ | |||
project: parseAsString(urlQuery['id']), | |||
category: parseAsString(urlQuery['category']) | |||
}); | |||
function ChangeVersionForm(props: Props) { | |||
return <ChangeEventForm {...props} changeEventButtonText="project_activity.change_version" />; | |||
} | |||
export const serializeQuery = (query: Query): Query => | |||
cleanQuery({ | |||
project: serializeString(query.project), | |||
category: serializeString(query.category) | |||
}); | |||
const mapStateToProps = null; | |||
const mapDispatchToProps = { changeEvent }; | |||
export default connect(mapStateToProps, mapDispatchToProps)(ChangeVersionForm); | |||
export const serializeUrlQuery = (query: Query): RawQuery => | |||
cleanQuery({ | |||
id: serializeString(query.project), | |||
category: serializeString(query.category) | |||
}); |
@@ -28,11 +28,12 @@ import VisualizationsContainer from '../visualizations/VisualizationsContainer'; | |||
import { parseUrlQuery } from '../store/utils'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import * as utils from '../utils'; | |||
import type { RawQuery } from '../../../helpers/query'; | |||
import '../styles.css'; | |||
type Props = {| | |||
isFavorite: boolean, | |||
location: { pathname: string, query: { [string]: string } }, | |||
location: { pathname: string, query: RawQuery }, | |||
fetchProjects: (query: string, isFavorite: boolean, organization?: {}) => Promise<*>, | |||
organization?: { key: string }, | |||
router: { | |||
@@ -43,7 +44,7 @@ type Props = {| | |||
|}; | |||
type State = { | |||
query: { [string]: string } | |||
query: RawQuery | |||
}; | |||
export default class AllProjects extends React.PureComponent { |
@@ -25,12 +25,13 @@ import AllProjectsContainer from './AllProjectsContainer'; | |||
import { getCurrentUser } from '../../../store/rootReducer'; | |||
import { isFavoriteSet, isAllSet } from '../utils'; | |||
import { searchProjects } from '../../../api/components'; | |||
import type { RawQuery } from '../../../helpers/query'; | |||
type Props = { | |||
currentUser: { isLoggedIn: boolean }, | |||
location: { query: {} }, | |||
router: { | |||
replace: (location: { pathname?: string, query?: { [string]: string } }) => void | |||
replace: (location: { pathname?: string, query?: RawQuery }) => void | |||
} | |||
}; | |||
@@ -22,13 +22,14 @@ import React from 'react'; | |||
import { IndexLink, Link } from 'react-router'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { saveAll, saveFavorite } from '../utils'; | |||
import type { RawQuery } from '../../../helpers/query'; | |||
type Props = { | |||
user: { | |||
isLoggedIn?: boolean | |||
}, | |||
organization?: { key: string }, | |||
query: { [string]: string } | |||
query: RawQuery | |||
}; | |||
export default class FavoriteFilter extends React.PureComponent { |
@@ -25,6 +25,7 @@ import Tooltip from '../../../components/controls/Tooltip'; | |||
import PerspectiveSelect from './PerspectiveSelect'; | |||
import ProjectsSortingSelect from './ProjectsSortingSelect'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import type { RawQuery } from '../../../helpers/query'; | |||
type Props = {| | |||
currentUser?: { isLoggedIn: boolean }, | |||
@@ -33,7 +34,7 @@ type Props = {| | |||
organization?: { key: string }, | |||
projects: Array<*>, | |||
projectsAppState: { loading: boolean, total?: number }, | |||
query: { [string]: string }, | |||
query: RawQuery, | |||
onSortChange: (sort: string, desc: boolean) => void, | |||
selectedSort: string, | |||
view: string, |
@@ -37,11 +37,12 @@ import SecurityFilter from '../filters/SecurityFilter'; | |||
import SizeFilter from '../filters/SizeFilter'; | |||
import TagsFilterContainer from '../filters/TagsFilterContainer'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import type { RawQuery } from '../../../helpers/query'; | |||
type Props = { | |||
isFavorite: boolean, | |||
organization?: { key: string }, | |||
query: { [string]: string }, | |||
query: RawQuery, | |||
view: string, | |||
visualization: string | |||
}; |
@@ -34,6 +34,7 @@ const getAsLevel = value => { | |||
return null; | |||
}; | |||
// TODO Maybe use parseAsString form helpers/query | |||
const getAsString = value => { | |||
if (!value) { | |||
return null; | |||
@@ -41,6 +42,7 @@ const getAsString = value => { | |||
return value; | |||
}; | |||
// TODO Maybe move it to helpers/query | |||
const getAsArray = (values, elementGetter) => { | |||
if (!values) { | |||
return null; |
@@ -0,0 +1,9 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`cleanQuery should remove undefined and null query items 1`] = ` | |||
Object { | |||
"a": "b", | |||
"d": "", | |||
"e": 0, | |||
} | |||
`; |
@@ -0,0 +1,84 @@ | |||
/* | |||
* 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 * as query from '../query'; | |||
describe('queriesEqual', () => { | |||
it('should correctly test equality of two queries', () => { | |||
expect(query.queriesEqual({ a: 'test', b: 'test' }, { a: 'test', b: 'test' })).toBeTruthy(); | |||
expect(query.queriesEqual({ a: [1, 2], b: 'test' }, { a: [1, 2], b: 'test' })).toBeTruthy(); | |||
expect(query.queriesEqual({ a: 'a' }, { a: 'test', b: 'test' })).toBeFalsy(); | |||
expect(query.queriesEqual({ a: [1, 2], b: 'test' }, { a: [1], b: 'test' })).toBeFalsy(); | |||
}); | |||
}); | |||
describe('cleanQuery', () => { | |||
it('should remove undefined and null query items', () => { | |||
expect(query.cleanQuery({ a: 'b', b: undefined, c: null, d: '', e: 0 })).toMatchSnapshot(); | |||
}); | |||
}); | |||
describe('parseAsBoolean', () => { | |||
it('should parse booleans correctly', () => { | |||
expect(query.parseAsBoolean('false')).toBeFalsy(); | |||
expect(query.parseAsBoolean('true')).toBeTruthy(); | |||
}); | |||
it('should return a default value', () => { | |||
expect(query.parseAsBoolean(1)).toBeTruthy(); | |||
expect(query.parseAsBoolean('foo')).toBeTruthy(); | |||
}); | |||
}); | |||
describe('parseAsFacetMode', () => { | |||
it('should facets modes correctly', () => { | |||
expect(query.parseAsFacetMode('debt')).toBe('effort'); | |||
expect(query.parseAsFacetMode('effort')).toBe('effort'); | |||
expect(query.parseAsFacetMode('count')).toBe('count'); | |||
expect(query.parseAsFacetMode('random')).toBe('count'); | |||
}); | |||
}); | |||
describe('parseAsString', () => { | |||
it('should parse strings correctly', () => { | |||
expect(query.parseAsString('random')).toBe('random'); | |||
expect(query.parseAsString('')).toBe(''); | |||
expect(query.parseAsString(null)).toBe(''); | |||
}); | |||
}); | |||
describe('parseAsArray', () => { | |||
it('should parse string arrays correctly', () => { | |||
expect(query.parseAsArray('1,2,3', query.parseAsString)).toEqual(['1', '2', '3']); | |||
}); | |||
}); | |||
describe('serializeString', () => { | |||
it('should serialize string correctly', () => { | |||
expect(query.serializeString('foo')).toBe('foo'); | |||
expect(query.serializeString('')).toBeUndefined(); | |||
}); | |||
}); | |||
describe('serializeStringArray', () => { | |||
it('should serialize array of string correctly', () => { | |||
expect(query.serializeStringArray(['1', '2', '3'])).toBe('1,2,3'); | |||
expect(query.serializeStringArray([])).toBeUndefined(); | |||
}); | |||
}); |
@@ -0,0 +1,68 @@ | |||
/* | |||
* 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 { isNil, omitBy } from 'lodash'; | |||
export type RawQuery = { [string]: string }; | |||
const arraysEqual = <T>(a: Array<T>, b: Array<T>) => { | |||
if (a.length !== b.length) { | |||
return false; | |||
} | |||
for (let i = 0; i < a.length; i++) { | |||
if (a[i] !== b[i]) { | |||
return false; | |||
} | |||
} | |||
return true; | |||
}; | |||
export const queriesEqual = <T>(a: T, b: T): boolean => { | |||
const keysA = Object.keys(a); | |||
const keysB = Object.keys(b); | |||
if (keysA.length !== keysB.length) { | |||
return false; | |||
} | |||
return keysA.every( | |||
key => | |||
(Array.isArray(a[key]) && Array.isArray(b[key]) | |||
? arraysEqual(a[key], b[key]) | |||
: a[key] === b[key]) | |||
); | |||
}; | |||
export const cleanQuery = (query: { [string]: ?string }): RawQuery => omitBy(query, isNil); | |||
export const parseAsBoolean = (value: ?string, defaultValue: boolean = true): boolean => | |||
(value === 'false' ? false : value === 'true' ? true : defaultValue); | |||
export const parseAsFacetMode = (facetMode: string) => | |||
(facetMode === 'debt' || facetMode === 'effort' ? 'effort' : 'count'); | |||
export const parseAsString = (value: ?string): string => value || ''; | |||
export const parseAsArray = <T>(value: ?string, itemParser: string => T): Array<T> => | |||
(value ? value.split(',').map(itemParser) : []); | |||
export const serializeString = (value: string): ?string => value || undefined; | |||
export const serializeStringArray = (value: ?Array<string>): ?string => | |||
(value && value.length ? value.join() : undefined); |
@@ -1,95 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`reducer 1`] = `Object {}`; | |||
exports[`reducer 2`] = ` | |||
Object { | |||
"AVgAgC1Vdo07z3PUnnkt": Object { | |||
"date": "2016-10-26T12:17:29+0200", | |||
"events": Array [ | |||
"AVkWNYNYr4pSN7TrXcjY", | |||
], | |||
"key": "AVgAgC1Vdo07z3PUnnkt", | |||
}, | |||
"AVgFqeOSKpGuA48ADATE": Object { | |||
"date": "2016-10-27T12:21:15+0200", | |||
"events": Array [], | |||
"key": "AVgFqeOSKpGuA48ADATE", | |||
}, | |||
"AVgGkRvCrrTJiPpCD-rG": Object { | |||
"date": "2016-10-27T16:33:50+0200", | |||
"events": Array [ | |||
"AVjUDBiSiXOcXjpycvde", | |||
], | |||
"key": "AVgGkRvCrrTJiPpCD-rG", | |||
}, | |||
} | |||
`; | |||
exports[`reducer 3`] = ` | |||
Object { | |||
"AVgAgC1Vdo07z3PUnnkt": Object { | |||
"date": "2016-10-26T12:17:29+0200", | |||
"events": Array [ | |||
"AVkWNYNYr4pSN7TrXcjY", | |||
], | |||
"key": "AVgAgC1Vdo07z3PUnnkt", | |||
}, | |||
"AVgFqeOSKpGuA48ADATE": Object { | |||
"date": "2016-10-27T12:21:15+0200", | |||
"events": Array [], | |||
"key": "AVgFqeOSKpGuA48ADATE", | |||
}, | |||
"AVgGkRvCrrTJiPpCD-rG": Object { | |||
"date": "2016-10-27T16:33:50+0200", | |||
"events": Array [ | |||
"AVjUDBiSiXOcXjpycvde", | |||
"AVkWcQ8Hr4pSN7TrXcjZ", | |||
], | |||
"key": "AVgGkRvCrrTJiPpCD-rG", | |||
}, | |||
} | |||
`; | |||
exports[`reducer 4`] = ` | |||
Object { | |||
"AVgAgC1Vdo07z3PUnnkt": Object { | |||
"date": "2016-10-26T12:17:29+0200", | |||
"events": Array [ | |||
"AVkWNYNYr4pSN7TrXcjY", | |||
], | |||
"key": "AVgAgC1Vdo07z3PUnnkt", | |||
}, | |||
"AVgFqeOSKpGuA48ADATE": Object { | |||
"date": "2016-10-27T12:21:15+0200", | |||
"events": Array [], | |||
"key": "AVgFqeOSKpGuA48ADATE", | |||
}, | |||
"AVgGkRvCrrTJiPpCD-rG": Object { | |||
"date": "2016-10-27T16:33:50+0200", | |||
"events": Array [ | |||
"AVjUDBiSiXOcXjpycvde", | |||
], | |||
"key": "AVgGkRvCrrTJiPpCD-rG", | |||
}, | |||
} | |||
`; | |||
exports[`reducer 5`] = ` | |||
Object { | |||
"AVgAgC1Vdo07z3PUnnkt": Object { | |||
"date": "2016-10-26T12:17:29+0200", | |||
"events": Array [ | |||
"AVkWNYNYr4pSN7TrXcjY", | |||
], | |||
"key": "AVgAgC1Vdo07z3PUnnkt", | |||
}, | |||
"AVgGkRvCrrTJiPpCD-rG": Object { | |||
"date": "2016-10-27T16:33:50+0200", | |||
"events": Array [ | |||
"AVjUDBiSiXOcXjpycvde", | |||
], | |||
"key": "AVgGkRvCrrTJiPpCD-rG", | |||
}, | |||
} | |||
`; |
@@ -1,57 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`reducer 1`] = `Object {}`; | |||
exports[`reducer 2`] = ` | |||
Object { | |||
"project-foo": Array [ | |||
"AVgFqeOSKpGuA48ADATE", | |||
"AVgAgC1Vdo07z3PUnnkt", | |||
], | |||
} | |||
`; | |||
exports[`reducer 3`] = ` | |||
Object { | |||
"project-foo": Array [ | |||
"AVgFqeOSKpGuA48ADATE", | |||
"AVgAgC1Vdo07z3PUnnkt", | |||
"AVgFqeOSKpGuA48ADATX", | |||
], | |||
} | |||
`; | |||
exports[`reducer 4`] = ` | |||
Object { | |||
"project-bar": Array [ | |||
"AVgGkRvCrrTJiPpCD-rG", | |||
], | |||
"project-foo": Array [ | |||
"AVgFqeOSKpGuA48ADATE", | |||
"AVgAgC1Vdo07z3PUnnkt", | |||
"AVgFqeOSKpGuA48ADATX", | |||
], | |||
} | |||
`; | |||
exports[`reducer 5`] = ` | |||
Object { | |||
"project-bar": Array [ | |||
"AVgGkRvCrrTJiPpCD-rG", | |||
], | |||
"project-foo": Array [ | |||
"AVgAgC1Vdo07z3PUnnkt", | |||
"AVgFqeOSKpGuA48ADATX", | |||
], | |||
} | |||
`; | |||
exports[`reducer 6`] = ` | |||
Object { | |||
"project-bar": Array [], | |||
"project-foo": Array [ | |||
"AVgAgC1Vdo07z3PUnnkt", | |||
"AVgFqeOSKpGuA48ADATX", | |||
], | |||
} | |||
`; |
@@ -1,77 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`actions addEvent 1`] = ` | |||
Object { | |||
"analysis": "foo", | |||
"event": Object { | |||
"key": "bar", | |||
}, | |||
"type": "ADD_PROJECT_ACTIVITY_EVENT", | |||
} | |||
`; | |||
exports[`actions changeEvent 1`] = ` | |||
Object { | |||
"changes": Object { | |||
"name": "bar", | |||
}, | |||
"event": "foo", | |||
"type": "CHANGE_PROJECT_ACTIVITY_EVENT", | |||
} | |||
`; | |||
exports[`actions deleteAnalysis 1`] = ` | |||
Object { | |||
"analysis": "bar", | |||
"project": "foo", | |||
"type": "DELETE_PROJECT_ACTIVITY_ANALYSIS", | |||
} | |||
`; | |||
exports[`actions deleteEvent 1`] = ` | |||
Object { | |||
"analysis": "foo", | |||
"event": "bar", | |||
"type": "DELETE_PROJECT_ACTIVITY_EVENT", | |||
} | |||
`; | |||
exports[`selectors getAnalyses 1`] = ` | |||
Array [ | |||
Object { | |||
"date": "2016-10-27T16:33:50+0200", | |||
"events": Array [ | |||
Object { | |||
"category": "VERSION", | |||
"key": "AVjUDBiSiXOcXjpycvde", | |||
"name": "2.18-SNAPSHOT", | |||
}, | |||
], | |||
"key": "AVgGkRvCrrTJiPpCD-rG", | |||
}, | |||
Object { | |||
"date": "2016-10-27T12:21:15+0200", | |||
"events": Array [], | |||
"key": "AVgFqeOSKpGuA48ADATE", | |||
}, | |||
Object { | |||
"date": "2016-10-26T12:17:29+0200", | |||
"events": Array [ | |||
Object { | |||
"category": "OTHER", | |||
"key": "AVkWNYNYr4pSN7TrXcjY", | |||
"name": "foo", | |||
}, | |||
], | |||
"key": "AVgAgC1Vdo07z3PUnnkt", | |||
}, | |||
] | |||
`; | |||
exports[`selectors getPaging 1`] = ` | |||
Object { | |||
"pageIndex": 1, | |||
"pageSize": 100, | |||
"total": 3, | |||
} | |||
`; |
@@ -1,73 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`reducer 1`] = `Object {}`; | |||
exports[`reducer 2`] = ` | |||
Object { | |||
"AVjUDBiSiXOcXjpycvde": Object { | |||
"category": "VERSION", | |||
"key": "AVjUDBiSiXOcXjpycvde", | |||
"name": "2.18-SNAPSHOT", | |||
}, | |||
"AVkWNYNYr4pSN7TrXcjY": Object { | |||
"category": "OTHER", | |||
"key": "AVkWNYNYr4pSN7TrXcjY", | |||
"name": "foo", | |||
}, | |||
} | |||
`; | |||
exports[`reducer 3`] = ` | |||
Object { | |||
"AVjUDBiSiXOcXjpycvde": Object { | |||
"category": "VERSION", | |||
"key": "AVjUDBiSiXOcXjpycvde", | |||
"name": "2.18-SNAPSHOT", | |||
}, | |||
"AVkWNYNYr4pSN7TrXcjY": Object { | |||
"category": "OTHER", | |||
"key": "AVkWNYNYr4pSN7TrXcjY", | |||
"name": "foo", | |||
}, | |||
"AVkWcQ8Hr4pSN7TrXcjZ": Object { | |||
"category": "OTHER", | |||
"key": "AVkWcQ8Hr4pSN7TrXcjZ", | |||
"name": "custom", | |||
}, | |||
} | |||
`; | |||
exports[`reducer 4`] = ` | |||
Object { | |||
"AVjUDBiSiXOcXjpycvde": Object { | |||
"category": "VERSION", | |||
"key": "AVjUDBiSiXOcXjpycvde", | |||
"name": "2.18-SNAPSHOT", | |||
}, | |||
"AVkWNYNYr4pSN7TrXcjY": Object { | |||
"category": "OTHER", | |||
"key": "AVkWNYNYr4pSN7TrXcjY", | |||
"name": "foo", | |||
}, | |||
"AVkWcQ8Hr4pSN7TrXcjZ": Object { | |||
"category": "OTHER", | |||
"key": "AVkWcQ8Hr4pSN7TrXcjZ", | |||
"name": "new name", | |||
}, | |||
} | |||
`; | |||
exports[`reducer 5`] = ` | |||
Object { | |||
"AVjUDBiSiXOcXjpycvde": Object { | |||
"category": "VERSION", | |||
"key": "AVjUDBiSiXOcXjpycvde", | |||
"name": "2.18-SNAPSHOT", | |||
}, | |||
"AVkWNYNYr4pSN7TrXcjY": Object { | |||
"category": "OTHER", | |||
"key": "AVkWNYNYr4pSN7TrXcjY", | |||
"name": "foo", | |||
}, | |||
} | |||
`; |
@@ -1,23 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`reducer 1`] = `Object {}`; | |||
exports[`reducer 2`] = ` | |||
Object { | |||
"project-foo": Object { | |||
"pageIndex": 1, | |||
"pageSize": 100, | |||
"total": 3, | |||
}, | |||
} | |||
`; | |||
exports[`reducer 3`] = ` | |||
Object { | |||
"project-foo": Object { | |||
"pageIndex": 2, | |||
"pageSize": 30, | |||
"total": 5, | |||
}, | |||
} | |||
`; |
@@ -1,90 +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 { configureTestStore } from '../../utils/configureStore'; | |||
import analyses, { getAnalysis } from '../analyses'; | |||
import { receiveProjectActivity, addEvent, deleteEvent, deleteAnalysis } from '../duck'; | |||
const PROJECT = 'project-foo'; | |||
const ANALYSES = [ | |||
{ | |||
key: 'AVgGkRvCrrTJiPpCD-rG', | |||
date: '2016-10-27T16:33:50+0200', | |||
events: [ | |||
{ | |||
key: 'AVjUDBiSiXOcXjpycvde', | |||
category: 'VERSION', | |||
name: '2.18-SNAPSHOT' | |||
} | |||
] | |||
}, | |||
{ | |||
key: 'AVgFqeOSKpGuA48ADATE', | |||
date: '2016-10-27T12:21:15+0200', | |||
events: [] | |||
}, | |||
{ | |||
key: 'AVgAgC1Vdo07z3PUnnkt', | |||
date: '2016-10-26T12:17:29+0200', | |||
events: [ | |||
{ | |||
key: 'AVkWNYNYr4pSN7TrXcjY', | |||
category: 'OTHER', | |||
name: 'foo' | |||
} | |||
] | |||
} | |||
]; | |||
const PAGING = { | |||
total: 3, | |||
pageIndex: 1, | |||
pageSize: 100 | |||
}; | |||
const NEW_EVENT = { | |||
key: 'AVkWcQ8Hr4pSN7TrXcjZ', | |||
category: 'OTHER', | |||
name: 'custom' | |||
}; | |||
it('reducer', () => { | |||
const store = configureTestStore(analyses); | |||
expect(store.getState()).toMatchSnapshot(); | |||
store.dispatch(receiveProjectActivity(PROJECT, ANALYSES, PAGING)); | |||
expect(store.getState()).toMatchSnapshot(); | |||
store.dispatch(addEvent(ANALYSES[0].key, NEW_EVENT)); | |||
expect(store.getState()).toMatchSnapshot(); | |||
store.dispatch(deleteEvent(ANALYSES[0].key, NEW_EVENT.key)); | |||
expect(store.getState()).toMatchSnapshot(); | |||
store.dispatch(deleteAnalysis(PROJECT, ANALYSES[1].key)); | |||
expect(store.getState()).toMatchSnapshot(); | |||
}); | |||
it('selector `getAnalysis`', () => { | |||
const analysis = ANALYSES[0]; | |||
const store = configureTestStore(analyses, { [analysis.key]: analysis }); | |||
expect(getAnalysis(store.getState(), analysis.key)).toBe(analysis); | |||
expect(getAnalysis(store.getState(), 'random')).toBeFalsy(); | |||
}); |
@@ -1,80 +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 { configureTestStore } from '../../utils/configureStore'; | |||
import analysesByProject from '../analysesByProject'; | |||
import { receiveProjectActivity, deleteAnalysis } from '../duck'; | |||
const PROJECT_FOO = 'project-foo'; | |||
const PROJECT_BAR = 'project-bar'; | |||
const ANALYSES_FOO = [ | |||
{ | |||
key: 'AVgFqeOSKpGuA48ADATE', | |||
date: '2016-10-27T12:21:15+0200', | |||
events: [] | |||
}, | |||
{ | |||
key: 'AVgAgC1Vdo07z3PUnnkt', | |||
date: '2016-10-26T12:17:29+0200', | |||
events: [] | |||
} | |||
]; | |||
const ANALYSES_FOO_2 = [ | |||
{ | |||
key: 'AVgFqeOSKpGuA48ADATX', | |||
date: '2016-10-27T12:21:15+0200', | |||
events: [] | |||
} | |||
]; | |||
const ANALYSES_BAR = [ | |||
{ | |||
key: 'AVgGkRvCrrTJiPpCD-rG', | |||
date: '2016-10-27T16:33:50+0200', | |||
events: [] | |||
} | |||
]; | |||
const PAGING = { | |||
total: 3, | |||
pageIndex: 1, | |||
pageSize: 100 | |||
}; | |||
it('reducer', () => { | |||
const store = configureTestStore(analysesByProject); | |||
expect(store.getState()).toMatchSnapshot(); | |||
store.dispatch(receiveProjectActivity(PROJECT_FOO, ANALYSES_FOO, PAGING)); | |||
expect(store.getState()).toMatchSnapshot(); | |||
store.dispatch(receiveProjectActivity(PROJECT_FOO, ANALYSES_FOO_2, { pageIndex: 2 })); | |||
expect(store.getState()).toMatchSnapshot(); | |||
store.dispatch(receiveProjectActivity(PROJECT_BAR, ANALYSES_BAR, PAGING)); | |||
expect(store.getState()).toMatchSnapshot(); | |||
store.dispatch(deleteAnalysis(PROJECT_FOO, 'AVgFqeOSKpGuA48ADATE')); | |||
expect(store.getState()).toMatchSnapshot(); | |||
store.dispatch(deleteAnalysis(PROJECT_BAR, 'AVgGkRvCrrTJiPpCD-rG')); | |||
expect(store.getState()).toMatchSnapshot(); | |||
}); |
@@ -1,99 +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 { configureTestStore } from '../../utils/configureStore'; | |||
import reducer, { | |||
receiveProjectActivity, | |||
getAnalyses, | |||
getPaging, | |||
addEvent, | |||
changeEvent, | |||
deleteEvent, | |||
deleteAnalysis | |||
} from '../duck'; | |||
const PROJECT = 'project-foo'; | |||
const ANALYSES = [ | |||
{ | |||
key: 'AVgGkRvCrrTJiPpCD-rG', | |||
date: '2016-10-27T16:33:50+0200', | |||
events: [ | |||
{ | |||
key: 'AVjUDBiSiXOcXjpycvde', | |||
category: 'VERSION', | |||
name: '2.18-SNAPSHOT' | |||
} | |||
] | |||
}, | |||
{ | |||
key: 'AVgFqeOSKpGuA48ADATE', | |||
date: '2016-10-27T12:21:15+0200', | |||
events: [] | |||
}, | |||
{ | |||
key: 'AVgAgC1Vdo07z3PUnnkt', | |||
date: '2016-10-26T12:17:29+0200', | |||
events: [ | |||
{ | |||
key: 'AVkWNYNYr4pSN7TrXcjY', | |||
category: 'OTHER', | |||
name: 'foo' | |||
} | |||
] | |||
} | |||
]; | |||
const PAGING = { | |||
total: 3, | |||
pageIndex: 1, | |||
pageSize: 100 | |||
}; | |||
describe('actions', () => { | |||
it('addEvent', () => { | |||
expect(addEvent('foo', { key: 'bar' })).toMatchSnapshot(); | |||
}); | |||
it('changeEvent', () => { | |||
expect(changeEvent('foo', { name: 'bar' })).toMatchSnapshot(); | |||
}); | |||
it('deleteEvent', () => { | |||
expect(deleteEvent('foo', 'bar')).toMatchSnapshot(); | |||
}); | |||
it('deleteAnalysis', () => { | |||
expect(deleteAnalysis('foo', 'bar')).toMatchSnapshot(); | |||
}); | |||
}); | |||
describe('selectors', () => { | |||
it('getAnalyses', () => { | |||
const store = configureTestStore(reducer); | |||
store.dispatch(receiveProjectActivity(PROJECT, ANALYSES, PAGING)); | |||
expect(getAnalyses(store.getState(), PROJECT)).toMatchSnapshot(); | |||
}); | |||
it('getPaging', () => { | |||
const store = configureTestStore(reducer); | |||
store.dispatch(receiveProjectActivity(PROJECT, ANALYSES, PAGING)); | |||
expect(getPaging(store.getState(), PROJECT)).toMatchSnapshot(); | |||
}); | |||
}); |
@@ -1,90 +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 { configureTestStore } from '../../utils/configureStore'; | |||
import events, { getEvent } from '../events'; | |||
import { receiveProjectActivity, addEvent, changeEvent, deleteEvent } from '../duck'; | |||
const PROJECT = 'project-foo'; | |||
const ANALYSES = [ | |||
{ | |||
key: 'AVgGkRvCrrTJiPpCD-rG', | |||
date: '2016-10-27T16:33:50+0200', | |||
events: [ | |||
{ | |||
key: 'AVjUDBiSiXOcXjpycvde', | |||
category: 'VERSION', | |||
name: '2.18-SNAPSHOT' | |||
} | |||
] | |||
}, | |||
{ | |||
key: 'AVgFqeOSKpGuA48ADATE', | |||
date: '2016-10-27T12:21:15+0200', | |||
events: [] | |||
}, | |||
{ | |||
key: 'AVgAgC1Vdo07z3PUnnkt', | |||
date: '2016-10-26T12:17:29+0200', | |||
events: [ | |||
{ | |||
key: 'AVkWNYNYr4pSN7TrXcjY', | |||
category: 'OTHER', | |||
name: 'foo' | |||
} | |||
] | |||
} | |||
]; | |||
const PAGING = { | |||
total: 3, | |||
pageIndex: 1, | |||
pageSize: 100 | |||
}; | |||
const NEW_EVENT = { | |||
key: 'AVkWcQ8Hr4pSN7TrXcjZ', | |||
category: 'OTHER', | |||
name: 'custom' | |||
}; | |||
it('reducer', () => { | |||
const store = configureTestStore(events); | |||
expect(store.getState()).toMatchSnapshot(); | |||
store.dispatch(receiveProjectActivity(PROJECT, ANALYSES, PAGING)); | |||
expect(store.getState()).toMatchSnapshot(); | |||
store.dispatch(addEvent(ANALYSES[0].key, NEW_EVENT)); | |||
expect(store.getState()).toMatchSnapshot(); | |||
store.dispatch(changeEvent(NEW_EVENT.key, { name: 'new name' })); | |||
expect(store.getState()).toMatchSnapshot(); | |||
store.dispatch(deleteEvent(ANALYSES[0].key, NEW_EVENT.key)); | |||
expect(store.getState()).toMatchSnapshot(); | |||
}); | |||
it('selector `getEvent`', () => { | |||
const event = ANALYSES[0].events[0]; | |||
const store = configureTestStore(events, { [event.key]: event }); | |||
expect(getEvent(store.getState(), event.key)).toBe(event); | |||
expect(getEvent(store.getState(), 'random')).toBeFalsy(); | |||
}); |
@@ -1,49 +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 { configureTestStore } from '../../utils/configureStore'; | |||
import paging from '../paging'; | |||
import { receiveProjectActivity } from '../duck'; | |||
const PROJECT = 'project-foo'; | |||
const ANALYSES = []; | |||
const PAGING_1 = { | |||
total: 3, | |||
pageIndex: 1, | |||
pageSize: 100 | |||
}; | |||
const PAGING_2 = { | |||
total: 5, | |||
pageIndex: 2, | |||
pageSize: 30 | |||
}; | |||
it('reducer', () => { | |||
const store = configureTestStore(paging); | |||
expect(store.getState()).toMatchSnapshot(); | |||
store.dispatch(receiveProjectActivity(PROJECT, ANALYSES, PAGING_1)); | |||
expect(store.getState()).toMatchSnapshot(); | |||
store.dispatch(receiveProjectActivity(PROJECT, ANALYSES, PAGING_2)); | |||
expect(store.getState()).toMatchSnapshot(); | |||
}); |
@@ -1,87 +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. | |||
*/ | |||
// @flow | |||
import { keyBy } from 'lodash'; | |||
import type { | |||
Action, | |||
ReceiveProjectActivityAction, | |||
AddEventAction, | |||
DeleteEventAction, | |||
DeleteAnalysisAction | |||
} from './duck'; | |||
type Analysis = { | |||
key: string, | |||
date: string, | |||
events: Array<string> | |||
}; | |||
export type State = { | |||
[key: string]: Analysis | |||
}; | |||
const receiveProjectActivity = (state: State, action: ReceiveProjectActivityAction): State => { | |||
const analysesWithFlatEvents = action.analyses.map(analysis => ({ | |||
...analysis, | |||
events: analysis.events.map(event => event.key) | |||
})); | |||
return { ...state, ...keyBy(analysesWithFlatEvents, 'key') }; | |||
}; | |||
const addEvent = (state: State, action: AddEventAction): State => { | |||
const analysis = state[action.analysis]; | |||
const newAnalysis = { | |||
...analysis, | |||
events: [...analysis.events, action.event.key] | |||
}; | |||
return { ...state, [action.analysis]: newAnalysis }; | |||
}; | |||
const deleteEvent = (state: State, action: DeleteEventAction): State => { | |||
const analysis = state[action.analysis]; | |||
const newAnalysis = { | |||
...analysis, | |||
events: analysis.events.filter(event => event !== action.event) | |||
}; | |||
return { ...state, [action.analysis]: newAnalysis }; | |||
}; | |||
const deleteAnalysis = (state: State, action: DeleteAnalysisAction): State => { | |||
const newState = { ...state }; | |||
delete newState[action.analysis]; | |||
return newState; | |||
}; | |||
export default (state: State = {}, action: Action): State => { | |||
switch (action.type) { | |||
case 'RECEIVE_PROJECT_ACTIVITY': | |||
return receiveProjectActivity(state, action); | |||
case 'ADD_PROJECT_ACTIVITY_EVENT': | |||
return addEvent(state, action); | |||
case 'DELETE_PROJECT_ACTIVITY_EVENT': | |||
return deleteEvent(state, action); | |||
case 'DELETE_PROJECT_ACTIVITY_ANALYSIS': | |||
return deleteAnalysis(state, action); | |||
default: | |||
return state; | |||
} | |||
}; | |||
export const getAnalysis = (state: State, key: string): Analysis => state[key]; |
@@ -1,53 +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. | |||
*/ | |||
// @flow | |||
import type { Action, ReceiveProjectActivityAction, DeleteAnalysisAction } from './duck'; | |||
export type State = { | |||
[key: string]: Array<string> | |||
}; | |||
const receiveProjectActivity = (state: State, action: ReceiveProjectActivityAction): State => { | |||
const analyses = state[action.project] || []; | |||
const newAnalyses = action.analyses.map(analysis => analysis.key); | |||
return { | |||
...state, | |||
[action.project]: action.paging.pageIndex === 1 ? newAnalyses : [...analyses, ...newAnalyses] | |||
}; | |||
}; | |||
const deleteAnalysis = (state: State, action: DeleteAnalysisAction): State => { | |||
const analyses = state[action.project]; | |||
return { | |||
...state, | |||
[action.project]: analyses.filter(key => key !== action.analysis) | |||
}; | |||
}; | |||
export default (state: State = {}, action: Action): State => { | |||
switch (action.type) { | |||
case 'RECEIVE_PROJECT_ACTIVITY': | |||
return receiveProjectActivity(state, action); | |||
case 'DELETE_PROJECT_ACTIVITY_ANALYSIS': | |||
return deleteAnalysis(state, action); | |||
default: | |||
return state; | |||
} | |||
}; |
@@ -1,144 +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. | |||
*/ | |||
// @flow | |||
import { combineReducers } from 'redux'; | |||
import analyses, * as fromAnalyses from './analyses'; | |||
import type { State as AnalysesState } from './analyses'; | |||
import analysesByProject from './analysesByProject'; | |||
import type { State as AnalysesByProjectState } from './analysesByProject'; | |||
import events, * as fromEvents from './events'; | |||
import type { State as EventsState } from './events'; | |||
import paging from './paging'; | |||
import type { State as PagingState } from './paging'; | |||
export type Event = { | |||
key: string, | |||
name: string, | |||
category: string, | |||
description?: string | |||
}; | |||
export type Analysis = { | |||
key: string, | |||
date: string, | |||
events: Array<Event> | |||
}; | |||
export type Paging = { | |||
total: number, | |||
pageIndex: number, | |||
pageSize: number | |||
}; | |||
export type ReceiveProjectActivityAction = { | |||
type: 'RECEIVE_PROJECT_ACTIVITY', | |||
project: string, | |||
analyses: Array<Analysis>, | |||
paging: Paging | |||
}; | |||
export type AddEventAction = { | |||
type: 'ADD_PROJECT_ACTIVITY_EVENT', | |||
analysis: string, | |||
event: Event | |||
}; | |||
export type DeleteEventAction = { | |||
type: 'DELETE_PROJECT_ACTIVITY_EVENT', | |||
analysis: string, | |||
event: string | |||
}; | |||
export type ChangeEventAction = { | |||
type: 'CHANGE_PROJECT_ACTIVITY_EVENT', | |||
event: string, | |||
changes: Object | |||
}; | |||
export type DeleteAnalysisAction = { | |||
type: 'DELETE_PROJECT_ACTIVITY_ANALYSIS', | |||
project: string, | |||
analysis: string | |||
}; | |||
export type Action = | |||
| ReceiveProjectActivityAction | |||
| AddEventAction | |||
| DeleteEventAction | |||
| ChangeEventAction | |||
| DeleteAnalysisAction; | |||
export const receiveProjectActivity = ( | |||
project: string, | |||
analyses: Array<Analysis>, | |||
paging: Paging | |||
): ReceiveProjectActivityAction => ({ | |||
type: 'RECEIVE_PROJECT_ACTIVITY', | |||
project, | |||
analyses, | |||
paging | |||
}); | |||
export const addEvent = (analysis: string, event: Event): AddEventAction => ({ | |||
type: 'ADD_PROJECT_ACTIVITY_EVENT', | |||
analysis, | |||
event | |||
}); | |||
export const deleteEvent = (analysis: string, event: string): DeleteEventAction => ({ | |||
type: 'DELETE_PROJECT_ACTIVITY_EVENT', | |||
analysis, | |||
event | |||
}); | |||
export const changeEvent = (event: string, changes: Object): ChangeEventAction => ({ | |||
type: 'CHANGE_PROJECT_ACTIVITY_EVENT', | |||
event, | |||
changes | |||
}); | |||
export const deleteAnalysis = (project: string, analysis: string): DeleteAnalysisAction => ({ | |||
type: 'DELETE_PROJECT_ACTIVITY_ANALYSIS', | |||
project, | |||
analysis | |||
}); | |||
type State = { | |||
analyses: AnalysesState, | |||
analysesByProject: AnalysesByProjectState, | |||
events: EventsState, | |||
filter: string, | |||
paging: PagingState | |||
}; | |||
export default combineReducers({ analyses, analysesByProject, events, paging }); | |||
const getEvent = (state: State, key: string): Event => fromEvents.getEvent(state.events, key); | |||
const getAnalysis = (state: State, key: string) => { | |||
const analysis = fromAnalyses.getAnalysis(state.analyses, key); | |||
const events: Array<Event> = analysis.events.map(key => getEvent(state, key)); | |||
return { ...analysis, events }; | |||
}; | |||
export const getAnalyses = (state: State, project: string) => | |||
state.analysesByProject[project] && | |||
state.analysesByProject[project].map(key => getAnalysis(state, key)); | |||
export const getPaging = (state: State, project: string) => state.paging[project]; |
@@ -1,77 +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. | |||
*/ | |||
// @flow | |||
import { keyBy } from 'lodash'; | |||
import type { | |||
Action, | |||
ReceiveProjectActivityAction, | |||
AddEventAction, | |||
DeleteEventAction, | |||
ChangeEventAction | |||
} from './duck'; | |||
export type State = { | |||
[key: string]: { | |||
key: string, | |||
name: string, | |||
category: string, | |||
description?: string | |||
} | |||
}; | |||
const receiveProjectActivity = (state: State, action: ReceiveProjectActivityAction): State => { | |||
const events = {}; | |||
action.analyses.forEach(analysis => { | |||
Object.assign(events, keyBy(analysis.events, 'key')); | |||
}); | |||
return { ...state, ...events }; | |||
}; | |||
const addEvent = (state: State, action: AddEventAction): State => { | |||
return { ...state, [action.event.key]: action.event }; | |||
}; | |||
const deleteEvent = (state: State, action: DeleteEventAction): State => { | |||
const newState = { ...state }; | |||
delete newState[action.event]; | |||
return newState; | |||
}; | |||
const changeEvent = (state: State, action: ChangeEventAction): State => { | |||
const newEvent = { ...state[action.event], ...action.changes }; | |||
return { ...state, [action.event]: newEvent }; | |||
}; | |||
export default (state: State = {}, action: Action): State => { | |||
switch (action.type) { | |||
case 'RECEIVE_PROJECT_ACTIVITY': | |||
return receiveProjectActivity(state, action); | |||
case 'ADD_PROJECT_ACTIVITY_EVENT': | |||
return addEvent(state, action); | |||
case 'DELETE_PROJECT_ACTIVITY_EVENT': | |||
return deleteEvent(state, action); | |||
case 'CHANGE_PROJECT_ACTIVITY_EVENT': | |||
return changeEvent(state, action); | |||
default: | |||
return state; | |||
} | |||
}; | |||
export const getEvent = (state: State, key: string) => state[key]; |
@@ -28,7 +28,6 @@ import notifications, * as fromNotifications from './notifications/duck'; | |||
import organizations, * as fromOrganizations from './organizations/duck'; | |||
import organizationsMembers, * as fromOrganizationsMembers from './organizationsMembers/reducer'; | |||
import globalMessages, * as fromGlobalMessages from './globalMessages/duck'; | |||
import projectActivity from './projectActivity/duck'; | |||
import measuresApp, * as fromMeasuresApp from '../apps/component-measures/store/rootReducer'; | |||
import permissionsApp, * as fromPermissionsApp from '../apps/permissions/shared/store/rootReducer'; | |||
import projectAdminApp, * as fromProjectAdminApp from '../apps/project-admin/store/rootReducer'; | |||
@@ -46,7 +45,6 @@ export default combineReducers({ | |||
notifications, | |||
organizations, | |||
organizationsMembers, | |||
projectActivity, | |||
users, | |||
// apps | |||
@@ -122,8 +120,6 @@ export const getOrganizationMembersLogins = (state, organization) => | |||
export const getOrganizationMembersState = (state, organization) => | |||
fromOrganizationsMembers.getOrganizationMembersState(state.organizationsMembers, organization); | |||
export const getProjectActivity = state => state.projectActivity; | |||
export const getProjects = state => fromProjectsApp.getProjects(state.projectsApp); | |||
export const getProjectsAppState = state => fromProjectsApp.getState(state.projectsApp); |