From: Siegfried Ehret <49895321+siegfried-ehret-sonarsource@users.noreply.github.com> Date: Mon, 27 May 2019 08:40:59 +0000 (+0200) Subject: SONARCLOUD-669 Fix favorite star behavior X-Git-Tag: 7.8~144 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=75a9d16ede11214ca31d8f75bff8c2247918376c;p=sonarqube.git SONARCLOUD-669 Fix favorite star behavior --- diff --git a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx index 8c52c9be55c..70b8d4657a1 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx @@ -172,6 +172,18 @@ export class AllProjects extends React.PureComponent { this.props.router.push({ pathname: this.props.location.pathname }); }; + handleFavorite = (key: string, isFavorite: boolean) => { + this.setState(({ projects }) => { + if (!projects) { + return null; + } + + return { + projects: projects.map(p => (p.key === key ? { ...p, isFavorite } : p)) + }; + }); + }; + handlePerspectiveChange = ({ view, visualization }: { view: string; visualization?: string }) => { const { storageOptionsSuffix } = this.props; const query: { @@ -317,6 +329,7 @@ export class AllProjects extends React.PureComponent { void; height: number; organization: T.Organization | undefined; project: Project; type?: string; } -export default function ProjectCard(props: Props) { - if (props.type === 'leak') { - return ; +export default class ProjectCard extends React.PureComponent { + render() { + if (this.props.type === 'leak') { + return ; + } + return ; } - return ; } diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx index cf0bce1ac04..beb338949ff 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx @@ -33,81 +33,90 @@ import { formatDuration } from '../utils'; import { getProjectUrl } from '../../../helpers/urls'; interface Props { + handleFavorite: (component: string, isFavorite: boolean) => void; height: number; organization: T.Organization | undefined; project: Project; } -export default function ProjectCardLeak({ height, organization, project }: Props) { - const { measures } = project; - const hasTags = project.tags.length > 0; - const periodMs = project.leakPeriodDate ? difference(Date.now(), project.leakPeriodDate) : 0; +export default class ProjectCardLeak extends React.PureComponent { + render() { + const { handleFavorite, height, organization, project } = this.props; + const { measures } = project; + const hasTags = project.tags.length > 0; + const periodMs = project.leakPeriodDate ? difference(Date.now(), project.leakPeriodDate) : 0; - return ( -
-
-
- {project.isFavorite != null && ( - - )} -

- {!organization && ( - + return ( +
+
+
+ {project.isFavorite != null && ( + )} - {project.name} -

- {project.analysisDate && } -
- +

+ {!organization && ( + + )} + + {project.name} + +

+ {project.analysisDate && } +
+ - {hasTags && } + {hasTags && } +
+ {project.analysisDate && project.leakPeriodDate && ( +
+ + {translateWithParameters('projects.new_code_period_x', formatDuration(periodMs))} + + + {formattedDate => ( + + {translateWithParameters('projects.last_analysis_on_x', formattedDate)} + + )} + +
+ )}
- {project.analysisDate && project.leakPeriodDate && ( -
- - {translateWithParameters('projects.new_code_period_x', formatDuration(periodMs))} - - - {formattedDate => ( - {translateWithParameters('projects.last_analysis_on_x', formattedDate)} + + {project.analysisDate && project.leakPeriodDate ? ( +
+ +
+ ) : ( +
+
+ + {project.analysisDate + ? translate('projects.no_new_code_period') + : translate('projects.not_analyzed')} + + {!project.analysisDate && ( + + {translate('projects.configure_analysis')} + )} - +
)}
- - {project.analysisDate && project.leakPeriodDate ? ( -
- -
- ) : ( -
-
- - {project.analysisDate - ? translate('projects.no_new_code_period') - : translate('projects.not_analyzed')} - - {!project.analysisDate && ( - - {translate('projects.configure_analysis')} - - )} -
-
- )} -
- ); + ); + } } diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx index c494faa25b4..214c6970ffb 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx @@ -31,73 +31,78 @@ import { Project } from '../types'; import { getProjectUrl } from '../../../helpers/urls'; interface Props { + handleFavorite: (component: string, isFavorite: boolean) => void; height: number; organization: T.Organization | undefined; project: Project; } -export default function ProjectCardOverall({ height, organization, project }: Props) { - const { measures } = project; +export default class ProjectCardOverall extends React.PureComponent { + render() { + const { handleFavorite, height, organization, project } = this.props; + const { measures } = project; - const hasTags = project.tags.length > 0; + const hasTags = project.tags.length > 0; - return ( -
-
-
- {project.isFavorite !== undefined && ( - - )} -

- {!organization && ( - + return ( +
+
+
+ {project.isFavorite !== undefined && ( + )} - {project.name} -

- {project.analysisDate && } -
- - {hasTags && } +

+ {!organization && ( + + )} + {project.name} +

+ {project.analysisDate && } +
+ + {hasTags && } +
+ {project.analysisDate && ( +
+ + {formattedDate => ( + + {translateWithParameters('projects.last_analysis_on_x', formattedDate)} + + )} + +
+ )}
- {project.analysisDate && ( -
- - {formattedDate => ( - - {translateWithParameters('projects.last_analysis_on_x', formattedDate)} - - )} - + + {project.analysisDate ? ( +
+ {} +
+ ) : ( +
+
+ {translate('projects.not_analyzed')} + + {translate('projects.configure_analysis')} + +
)}
- - {project.analysisDate ? ( -
- {} -
- ) : ( -
-
- {translate('projects.not_analyzed')} - - {translate('projects.configure_analysis')} - -
-
- )} -
- ); + ); + } } diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectsList.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectsList.tsx index ac05a3a5862..7225fdedf85 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectsList.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectsList.tsx @@ -33,6 +33,7 @@ import { OnboardingContext } from '../../../app/components/OnboardingContext'; interface Props { cardType?: string; currentUser: T.CurrentUser; + handleFavorite: (component: string, isFavorite: boolean) => void; isFavorite: boolean; isFiltered: boolean; organization: T.Organization | undefined; @@ -75,6 +76,7 @@ export default class ProjectsList extends React.PureComponent { return (
{ expect(wrapper).toMatchSnapshot(); }); +it('handles favorite projects', () => { + const wrapper = shallowRender(); + expect(wrapper.state('projects')).toMatchSnapshot(); + + wrapper.instance().handleFavorite('foo', true); + expect(wrapper.state('projects')).toMatchSnapshot(); +}); + function shallowRender( props: Partial = {}, push = jest.fn(), replace = jest.fn() ) { - const wrapper = shallow( + const wrapper = shallow( by default', () => { + const wrapper = shallowRender(); + expect(wrapper.find('ProjectCardOverall')).toBeTruthy(); + expect(wrapper.find('ProjectCardLeak')).toBeTruthy(); +}); + +it('should show when asked', () => { + const wrapper = shallowRender(); + expect(wrapper.find('ProjectCardLeak')).toBeTruthy(); + expect(wrapper.find('ProjectCardOverall')).toBeTruthy(); +}); + +function shallowRender(type?: string) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLeak-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLeak-test.tsx index 7e455598b3c..59df2a1b6e5 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLeak-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLeak-test.tsx @@ -46,7 +46,7 @@ const PROJECT: Project = { }; it('should display analysis date and leak start date', () => { - const card = shallow(); + const card = shallowRender(PROJECT); expect(card.find('.project-card-dates').exists()).toBeTruthy(); expect(card.find('.project-card-dates').find('.project-card-leak-date')).toHaveLength(1); expect(card.find('.project-card-dates').find('DateTimeFormatter')).toHaveLength(1); @@ -54,14 +54,14 @@ it('should display analysis date and leak start date', () => { it('should not display analysis date or leak start date', () => { const project = { ...PROJECT, analysisDate: undefined }; - const card = shallow(); + const card = shallowRender(project); expect(card.find('.project-card-dates').exists()).toBeFalsy(); }); it('should display tags', () => { const project = { ...PROJECT, tags: ['foo', 'bar'] }; expect( - shallow() + shallowRender(project) .find('TagsList') .exists() ).toBeTruthy(); @@ -70,26 +70,27 @@ it('should display tags', () => { it('should display private badge', () => { const project: Project = { ...PROJECT, visibility: 'private' }; expect( - shallow() + shallowRender(project) .find('Connect(PrivacyBadge)') .exists() ).toBeTruthy(); }); it('should display the leak measures and quality gate', () => { - expect( - shallow() - ).toMatchSnapshot(); + expect(shallowRender(PROJECT)).toMatchSnapshot(); }); it('should display not analyzed yet', () => { - expect( - shallow( - - ) - ).toMatchSnapshot(); + expect(shallowRender({ ...PROJECT, analysisDate: undefined })).toMatchSnapshot(); }); + +function shallowRender(project: Project) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverall-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverall-test.tsx index bc3eef5411d..827d89e4aef 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverall-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverall-test.tsx @@ -41,18 +41,12 @@ const PROJECT: Project = { it('should display analysis date (and not leak period) when defined', () => { expect( - shallow() + shallowRender(PROJECT) .find('.project-card-dates') .exists() ).toBeTruthy(); expect( - shallow( - - ) + shallowRender({ ...PROJECT, analysisDate: undefined }) .find('.project-card-dates') .exists() ).toBeFalsy(); @@ -61,7 +55,7 @@ it('should display analysis date (and not leak period) when defined', () => { it('should not display the quality gate', () => { const project = { ...PROJECT, analysisDate: undefined }; expect( - shallow() + shallowRender(project) .find('ProjectCardOverallQualityGate') .exists() ).toBeFalsy(); @@ -70,7 +64,7 @@ it('should not display the quality gate', () => { it('should display tags', () => { const project = { ...PROJECT, tags: ['foo', 'bar'] }; expect( - shallow() + shallowRender(project) .find('TagsList') .exists() ).toBeTruthy(); @@ -79,26 +73,27 @@ it('should display tags', () => { it('should display private badge', () => { const project: Project = { ...PROJECT, visibility: 'private' }; expect( - shallow() + shallowRender(project) .find('Connect(PrivacyBadge)') .exists() ).toBeTruthy(); }); it('should display the overall measures and quality gate', () => { - expect( - shallow() - ).toMatchSnapshot(); + expect(shallowRender(PROJECT)).toMatchSnapshot(); }); it('should display not analyzed yet', () => { - expect( - shallow( - - ) - ).toMatchSnapshot(); + expect(shallowRender({ ...PROJECT, analysisDate: undefined })).toMatchSnapshot(); }); + +function shallowRender(project: Project) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap index d41f0b2801d..28726c14b96 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap @@ -1,5 +1,30 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`handles favorite projects 1`] = ` +Array [ + Object { + "key": "foo", + "measures": Object {}, + "name": "Foo", + "tags": Array [], + "visibility": "public", + }, +] +`; + +exports[`handles favorite projects 2`] = ` +Array [ + Object { + "isFavorite": true, + "key": "foo", + "measures": Object {}, + "name": "Foo", + "tags": Array [], + "visibility": "public", + }, +] +`; + exports[`renders 1`] = `
void; } -export default function Favorite({ component, ...other }: Props) { - return ( - addFavorite(component)} - removeFavorite={() => removeFavorite(component)} - /> - ); +export default class Favorite extends React.PureComponent { + callback = (isFavorite: boolean) => + this.props.handleFavorite && this.props.handleFavorite(this.props.component, isFavorite); + + add = () => { + return addFavorite(this.props.component).then(() => this.callback(true)); + }; + + remove = () => { + return removeFavorite(this.props.component).then(() => this.callback(false)); + }; + + render() { + const { component, handleFavorite, ...other } = this.props; + return ; + } } diff --git a/server/sonar-web/src/main/js/components/controls/FavoriteBase.tsx b/server/sonar-web/src/main/js/components/controls/FavoriteBase.tsx index 0566fbdfd85..10865ee16c7 100644 --- a/server/sonar-web/src/main/js/components/controls/FavoriteBase.tsx +++ b/server/sonar-web/src/main/js/components/controls/FavoriteBase.tsx @@ -47,12 +47,6 @@ export default class FavoriteBase extends React.PureComponent { this.mounted = true; } - componentWillReceiveProps(nextProps: Props) { - if (nextProps.favorite !== this.props.favorite || nextProps.favorite !== this.state.favorite) { - this.setState({ favorite: nextProps.favorite }); - } - } - componentWillUnmount() { this.mounted = false; } diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/Favorite-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/Favorite-test.tsx index c57716e3871..009f15f6e30 100644 --- a/server/sonar-web/src/main/js/components/controls/__tests__/Favorite-test.tsx +++ b/server/sonar-web/src/main/js/components/controls/__tests__/Favorite-test.tsx @@ -21,6 +21,33 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import Favorite from '../Favorite'; +jest.mock('../../../api/favorites', () => ({ + addFavorite: jest.fn(() => Promise.resolve()), + removeFavorite: jest.fn(() => Promise.resolve()) +})); + it('renders', () => { - expect(shallow()).toMatchSnapshot(); + expect(shallowRender()).toMatchSnapshot(); +}); + +it('calls handleFavorite when given', async () => { + const handleFavorite = jest.fn(); + const wrapper = shallowRender(handleFavorite); + const favoriteBase = wrapper.find('FavoriteBase'); + const addFavorite = favoriteBase.prop('addFavorite'); + const removeFavorite = favoriteBase.prop('removeFavorite'); + + removeFavorite(); + await new Promise(setImmediate); + expect(handleFavorite).toHaveBeenCalledWith('foo', false); + + addFavorite(); + await new Promise(setImmediate); + expect(handleFavorite).toHaveBeenCalledWith('foo', true); }); + +function shallowRender(handleFavorite?: () => void) { + return shallow( + + ); +}