Quellcode durchsuchen

SONARCLOUD-270 Show latest notification in the Navbar

tags/7.6
Wouter Admiraal vor 5 Jahren
Ursprung
Commit
893a66d84f

+ 68
- 2
server/sonar-web/src/main/js/api/news.ts Datei anzeigen

@@ -30,10 +30,48 @@ export interface PrismicNews {
uid: string;
}

interface PrismicResult {
data: {
notification: string;
publication_date: string;
body: PrismicResultFeature[];
};
}

interface PrismicResultFeature {
items: Array<{
category: {
data: {
color: string;
name: string;
};
};
}>;
primary: {
description: string;
read_more_link: {
url?: string;
};
};
}

export interface PrismicFeatureNews {
notification: string;
publicationDate: string;
features: Array<{
categories: Array<{
color: string;
name: string;
}>;
description: string;
readMore?: string;
}>;
}

const PRISMIC_API_URL = 'https://sonarsource.cdn.prismic.io/api/v2';

export function fetchPrismicRefs() {
return getCorsJSON(PRISMIC_API_URL).then((response: { refs: Array<PrismicRef> }) => {
return getCorsJSON(PRISMIC_API_URL).then((response: { refs: PrismicRef[] }) => {
const master = response && response.refs.find(ref => ref.id === 'master');
if (!master) {
return Promise.reject('No master ref found');
@@ -58,5 +96,33 @@ export function fetchPrismicNews(data: {
pageSize: data.ps || 1,
q,
ref: data.ref
}).then(({ results }: { results: Array<PrismicNews> }) => results);
}).then(({ results }: { results: PrismicNews[] }) => results);
}

export function fetchPrismicFeatureNews(data: {
accessToken: string;
ps?: number;
ref: string;
}): Promise<PrismicFeatureNews[]> {
const q = ['[[at(document.type, "sc_product_news")]]'];
return getCorsJSON(PRISMIC_API_URL + '/documents/search', {
access_token: data.accessToken,
orderings: '[document.first_publication_date desc]',
pageSize: data.ps || 1,
q,
fetchLinks: 'sc_category.color,sc_category.name',
ref: data.ref
}).then(({ results }: { results: PrismicResult[] }) => {
return results.map(result => {
return {
notification: result.data.notification,
publicationDate: result.data.publication_date,
features: result.data.body.map(feature => ({
categories: feature.items.map(item => item.category.data),
description: feature.primary.description,
readMore: feature.primary.read_more_link.url
}))
};
});
});
}

+ 41
- 0
server/sonar-web/src/main/js/api/user-settings.ts Datei anzeigen

@@ -0,0 +1,41 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 { getJSON, post } from '../helpers/request';
import throwGlobalError from '../app/utils/throwGlobalError';

export function setUserSetting(data: T.CurrentUserSettingData) {
return post('/api/user_settings/set', data)
.catch(() => Promise.resolve()) // TODO Remove mock.
.catch(throwGlobalError);
}

export function listUserSettings(): Promise<{ userSettings: T.CurrentUserSettingData[] }> {
return getJSON('/api/user_settings/list')
.catch(() => {
// TODO Remove mock.
return {
userSettings: [
{ key: 'notificationsReadDate', value: '2018-12-01T12:07:19+0000' },
{ key: 'notificationsOptOut', value: 'false' }
]
};
})
.catch(throwGlobalError);
}

+ 5
- 1
server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx Datei anzeigen

@@ -78,7 +78,11 @@ export default class EmbedDocsPopupHelper extends React.PureComponent<{}, State>
onRequestClose={this.closeHelp}
open={this.state.helpOpen}
overlay={<EmbedDocsPopup onClose={this.closeHelp} />}>
<a className="navbar-help" href="#" onClick={this.handleClick} title={translate('help')}>
<a
className="navbar-help navbar-icon"
href="#"
onClick={this.handleClick}
title={translate('help')}>
<HelpIcon />
</a>
</Toggler>

+ 63
- 2
server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.css Datei anzeigen

@@ -50,8 +50,7 @@
border: none !important;
}

.navbar-help,
.navbar-plus {
.navbar-icon {
display: inline-block;
height: var(--globalNavHeight);
padding: calc(var(--globalNavHeight) - var(--globalNavContentHeight)) 12px !important;
@@ -98,6 +97,68 @@
.global-navbar-menu-right {
flex: 1;
justify-content: flex-end;
margin-left: calc(5 * var(--gridSize));
}

.navbar-latest-notification {
flex: 0 1 350px;
text-align: right;
overflow: hidden;
}

.navbar-latest-notification-wrapper {
position: relative;
display: inline-block;
padding: var(--gridSize) 34px var(--gridSize) 50px;
height: 28px;
max-width: 100%;
box-sizing: border-box;
overflow: hidden;
vertical-align: middle;
font-size: var(--smallFontSize);
color: var(--sonarcloudBlack500);
background-color: black;
text-overflow: ellipsis;
white-space: nowrap;
border-radius: 3px;
cursor: pointer;
}

.navbar-latest-notification-wrapper:hover {
color: var(--sonarcloudBlack300);
}

.navbar-latest-notification-wrapper .badge {
position: absolute;
height: 18px;
margin-right: var(--gridSize);
left: calc(var(--gridSize) / 2);
top: 5px;
font-size: var(--verySmallFontSize);
text-transform: uppercase;
background-color: var(--lightBlue);
color: var(--darkBlue);
}

.navbar-latest-notification-wrapper .label {
display: block;
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.navbar-latest-notification .navbar-icon {
position: absolute;
right: 0;
top: 0;
height: 28px;
padding: 9px var(--gridSize) !important;
border-left: 2px solid #262626;
}

.navbar-latest-notification .navbar-icon:hover path {
fill: var(--sonarcloudBlack300) !important;
}

.global-navbar-menu-right .navbar-search {

+ 2
- 0
server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx Datei anzeigen

@@ -22,6 +22,7 @@ import { connect } from 'react-redux';
import GlobalNavBranding, { SonarCloudNavBranding } from './GlobalNavBranding';
import GlobalNavMenu from './GlobalNavMenu';
import GlobalNavExplore from './GlobalNavExplore';
import GlobalNavNotifications from './GlobalNavNotifications';
import GlobalNavUserContainer from './GlobalNavUserContainer';
import Search from '../../search/Search';
import EmbedDocsPopupHelper from '../../embed-docs-modal/EmbedDocsPopupHelper';
@@ -57,6 +58,7 @@ export class GlobalNav extends React.PureComponent<Props> {
<GlobalNavMenu {...this.props} />

<ul className="global-navbar-menu global-navbar-menu-right">
{isSonarCloud() && <GlobalNavNotifications />}
{isSonarCloud() && <GlobalNavExplore location={this.props.location} />}
<EmbedDocsPopupHelper />
<Search appState={appState} currentUser={currentUser} />

+ 152
- 0
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavNotifications.tsx Datei anzeigen

@@ -0,0 +1,152 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 React from 'react';
import { connect } from 'react-redux';
import ClearIcon from '../../../../components/icons-components/ClearIcon';
import NotificationIcon from '../../../../components/icons-components/NotificationIcon';
import { sonarcloudBlack500 } from '../../../theme';
import {
fetchPrismicRefs,
fetchPrismicFeatureNews,
PrismicFeatureNews
} from '../../../../api/news';
import { differenceInSeconds, parseDate } from '../../../../helpers/dates';
import { translate } from '../../../../helpers/l10n';
import { fetchCurrentUserSettings, setCurrentUserSetting } from '../../../../store/users';
import {
getGlobalSettingValue,
getCurrentUserSettings,
Store
} from '../../../../store/rootReducer';

interface Props {
accessToken?: string;
fetchCurrentUserSettings: () => void;
notificationsLastReadDate?: Date;
notificationsOptOut?: boolean;
setCurrentUserSetting: (setting: T.CurrentUserSettingData) => void;
}

interface State {
news: PrismicFeatureNews[];
ready: boolean;
}

export class GlobalNavNotifications extends React.PureComponent<Props, State> {
mounted = false;
state: State = { news: [], ready: false };

componentDidMount() {
this.mounted = true;
this.fetchPrismicFeatureNews();
this.props.fetchCurrentUserSettings();
}

componentWillUnmount() {
this.mounted = false;
}

checkHasUnread = () => {
const lastNews = this.state.news[0];
if (!lastNews) {
return false;
}

const { notificationsLastReadDate } = this.props;
return (
!notificationsLastReadDate ||
differenceInSeconds(parseDate(lastNews.publicationDate), notificationsLastReadDate) > 0
);
};

fetchPrismicFeatureNews = () => {
const { accessToken } = this.props;
if (accessToken) {
fetchPrismicRefs()
.then(({ ref }) => fetchPrismicFeatureNews({ accessToken, ref, ps: 10 }))
.then(
news => {
if (this.mounted && news) {
this.setState({ ready: true, news });
}
},
() => {}
);
}
};

handleDismiss = (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
this.props.setCurrentUserSetting({
key: 'notificationsReadDate',
value: new Date().toISOString()
});
};

render() {
if (!this.state.ready) {
return null;
}

const { notificationsOptOut } = this.props;
const lastNews = this.state.news[0];
const hasUnread = this.checkHasUnread();
const showNotifications = Boolean(!notificationsOptOut && lastNews && hasUnread);
return (
<>
{showNotifications && (
<li className="navbar-latest-notification">
<div className="navbar-latest-notification-wrapper">
<span className="badge">{translate('new')}</span>
<span className="label">{lastNews.notification}</span>
<a className="navbar-icon" href="#" onClick={this.handleDismiss}>
<ClearIcon fill={sonarcloudBlack500} size={10} />
</a>
</div>
</li>
)}
<li>
<a className="navbar-icon">
<NotificationIcon hasUnread={hasUnread && !notificationsOptOut} />
</a>
</li>
</>
);
}
}

const mapStateToProps = (state: Store) => {
const accessToken = getGlobalSettingValue(state, 'sonar.prismic.accessToken');
const userSettings = getCurrentUserSettings(state);
return {
accessToken: accessToken && accessToken.value,
notificationsLastReadDate: userSettings.notificationsReadDate
? parseDate(userSettings.notificationsReadDate)
: undefined,
notificationsOptOut: userSettings.notificationsReadDate === 'true'
};
};

const mapDispatchToProps = { fetchCurrentUserSettings, setCurrentUserSetting };

export default connect(
mapStateToProps,
mapDispatchToProps
)(GlobalNavNotifications);

+ 1
- 1
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavPlus.tsx Datei anzeigen

@@ -176,7 +176,7 @@ export class GlobalNavPlus extends React.PureComponent<Props & WithRouterProps,
}
tagName="li">
<a
className="navbar-plus"
className="navbar-icon navbar-plus"
href="#"
title={
isSonarCloud()

+ 121
- 0
server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavNotifications-test.tsx Datei anzeigen

@@ -0,0 +1,121 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 React from 'react';
import { shallow } from 'enzyme';
import { GlobalNavNotifications } from '../GlobalNavNotifications';
import { waitAndUpdate } from '../../../../../helpers/testUtils';
import {
fetchPrismicRefs,
fetchPrismicFeatureNews,
PrismicFeatureNews
} from '../../../../../api/news';
import { parseDate } from '../../../../../helpers/dates';

// Solve redux warning issue "No reducer provided for key":
// https://stackoverflow.com/questions/43375079/redux-warning-only-appearing-in-tests
jest.mock('../../../../../store/rootReducer');

jest.mock('../../../../../api/news', () => {
const prismicResult: PrismicFeatureNews[] = [
{
notification: '10 Java rules, Github checks, Security Hotspots, BitBucket branch decoration',
publicationDate: '2018-04-06',
features: [
{
categories: [{ color: '#ff0000', name: 'Java' }],
description: '10 new Java rules'
}
]
},
{
notification: 'Some other notification',
publicationDate: '2018-04-05',
features: [
{
categories: [{ color: '#0000ff', name: 'BitBucket' }],
description: 'BitBucket branch decoration',
readMore: 'http://example.com'
}
]
}
];

return {
fetchPrismicRefs: jest.fn().mockResolvedValue({ ref: 'master-ref' }),
fetchPrismicFeatureNews: jest.fn().mockResolvedValue(prismicResult)
};
});

beforeEach(() => {
(fetchPrismicRefs as jest.Mock).mockClear();
(fetchPrismicFeatureNews as jest.Mock).mockClear();
});

it('should render correctly if there are new features, and the user has not opted out', async () => {
const wrapper = shallowRender();
expect(wrapper.type()).toBeNull();

await waitAndUpdate(wrapper);
expect(fetchPrismicRefs).toHaveBeenCalled();
expect(fetchPrismicFeatureNews).toHaveBeenCalled();
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('.navbar-latest-notification')).toHaveLength(1);
});

it('should render correctly if there are new features, but the user has opted out', async () => {
const wrapper = shallowRender({ notificationsOptOut: true });

await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('.navbar-latest-notification')).toHaveLength(0);
});

it('should render correctly if there are no new unread features', async () => {
const wrapper = shallowRender({
notificationsLastReadDate: parseDate('2018-12-31T12:07:19+0000')
});

await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('.navbar-latest-notification')).toHaveLength(0);
});

it('should render correctly if there are no new features', async () => {
(fetchPrismicFeatureNews as jest.Mock<any>).mockResolvedValue([]);

const wrapper = shallowRender();

await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('.navbar-latest-notification')).toHaveLength(0);
});

function shallowRender(props: Partial<GlobalNavNotifications['props']> = {}) {
return shallow(
<GlobalNavNotifications
accessToken="token"
fetchCurrentUserSettings={jest.fn()}
notificationsLastReadDate={parseDate('2018-01-01T12:07:19+0000')}
notificationsOptOut={false}
setCurrentUserSetting={jest.fn()}
{...props}
/>
);
}

+ 1
- 0
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap Datei anzeigen

@@ -30,6 +30,7 @@ exports[`should render for SonarCloud 1`] = `
<ul
className="global-navbar-menu global-navbar-menu-right"
>
<Connect(GlobalNavNotifications) />
<GlobalNavExplore
location={
Object {

+ 85
- 0
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavNotifications-test.tsx.snap Datei anzeigen

@@ -0,0 +1,85 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly if there are new features, and the user has not opted out 1`] = `
<Fragment>
<li
className="navbar-latest-notification"
>
<div
className="navbar-latest-notification-wrapper"
>
<span
className="badge"
>
new
</span>
<span
className="label"
>
10 Java rules, Github checks, Security Hotspots, BitBucket branch decoration
</span>
<a
className="navbar-icon"
href="#"
onClick={[Function]}
>
<ClearIcon
fill="#8a8c8f"
size={10}
/>
</a>
</div>
</li>
<li>
<a
className="navbar-icon"
>
<NotificationIcon
hasUnread={true}
/>
</a>
</li>
</Fragment>
`;

exports[`should render correctly if there are new features, but the user has opted out 1`] = `
<Fragment>
<li>
<a
className="navbar-icon"
>
<NotificationIcon
hasUnread={false}
/>
</a>
</li>
</Fragment>
`;

exports[`should render correctly if there are no new features 1`] = `
<Fragment>
<li>
<a
className="navbar-icon"
>
<NotificationIcon
hasUnread={false}
/>
</a>
</li>
</Fragment>
`;

exports[`should render correctly if there are no new unread features 1`] = `
<Fragment>
<li>
<a
className="navbar-icon"
>
<NotificationIcon
hasUnread={false}
/>
</a>
</li>
</Fragment>
`;

+ 1
- 1
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavPlus-test.tsx.snap Datei anzeigen

@@ -20,7 +20,7 @@ exports[`render 1`] = `
tagName="li"
>
<a
className="navbar-plus"
className="navbar-icon navbar-plus"
href="#"
title="my_account.create_new_project_portfolio_or_application"
>

+ 9
- 0
server/sonar-web/src/main/js/app/types.d.ts Datei anzeigen

@@ -209,6 +209,15 @@ declare namespace T {
showOnboardingTutorial?: boolean;
}

export type CurrentUserSettings = { [key in CurrentUserSettingNames]?: string };

export interface CurrentUserSettingData {
key: CurrentUserSettingNames;
value: string;
}

type CurrentUserSettingNames = 'notificationsOptOut' | 'notificationsReadDate';

export interface CustomMeasure {
createdAt?: string;
description?: string;

+ 52
- 0
server/sonar-web/src/main/js/components/icons-components/NotificationIcon.tsx Datei anzeigen

@@ -0,0 +1,52 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 React from 'react';
import Icon, { IconProps } from './Icon';
import { blue } from '../../app/theme';

interface Props extends IconProps {
hasUnread?: boolean;
}

export default function NotificationIcon({
className,
fill = 'currentColor',
hasUnread,
size
}: Props) {
return (
<Icon className={className} size={size}>
{hasUnread ? (
<>
<path
d="M8 1a.875.875 0 0 0-.875.875v.57c-2.009.418-3.498 2.118-3.498 4.242 0 2.798-.987 3.652-1.516 4.22a.856.856 0 0 0-.236.593.875.875 0 0 0 .877.875h10.496a.875.875 0 0 0 .877-.875.854.854 0 0 0-.236-.594c-.497-.534-1.388-1.342-1.494-3.76a2.814 2.814 0 0 1-.768.108A2.814 2.814 0 0 1 8.814 4.44a2.814 2.814 0 0 1 .665-1.818 4.543 4.543 0 0 0-.604-.178v-.57A.875.875 0 0 0 8 1zM6.25 13.25a1.75 1.75 0 0 0 3.5 0h-3.5z"
style={{ fill }}
/>
<circle cx="11.627" cy="4.441" r="2" style={{ fill: blue }} />
</>
) : (
<path
d="M8 15a1.75 1.75 0 0 0 1.75-1.75h-3.5c0 .967.784 1.75 1.75 1.75zm5.89-4.094c-.529-.567-1.517-1.421-1.517-4.218 0-2.125-1.49-3.826-3.499-4.243v-.57a.875.875 0 1 0-1.748 0v.57c-2.01.417-3.499 2.118-3.499 4.243 0 2.797-.988 3.65-1.517 4.218a.854.854 0 0 0-.235.594.876.876 0 0 0 .878.875h10.494a.876.876 0 0 0 .878-.875.853.853 0 0 0-.235-.594z"
style={{ fill }}
/>
)}
</Icon>
);
}

+ 4
- 0
server/sonar-web/src/main/js/store/rootReducer.ts Datei anzeigen

@@ -65,6 +65,10 @@ export function getLanguages(state: Store) {
return fromLanguages.getLanguages(state.languages);
}

export function getCurrentUserSettings(state: Store) {
return fromUsers.getCurrentUserSettings(state.users);
}

export function getCurrentUser(state: Store) {
return fromUsers.getCurrentUser(state.users);
}

+ 67
- 20
server/sonar-web/src/main/js/store/users.ts Datei anzeigen

@@ -21,14 +21,54 @@ import { uniq } from 'lodash';
import { Dispatch, combineReducers } from 'redux';
import { ActionType } from './utils/actions';
import * as api from '../api/users';
import { listUserSettings, setUserSetting } from '../api/user-settings';
import { isLoggedIn } from '../helpers/users';

const enum Actions {
ReceiveCurrentUser = 'RECEIVE_CURRENT_USER',
ReceiveCurrentUserSettings = 'RECEIVE_CURRENT_USER_SETTINGS',
SkipOnboardingAction = 'SKIP_ONBOARDING',
SetHomePageAction = 'SET_HOMEPAGE'
}

type Action =
| ActionType<typeof receiveCurrentUser, Actions.ReceiveCurrentUser>
| ActionType<typeof receiveCurrentUserSettings, Actions.ReceiveCurrentUserSettings>
| ActionType<typeof setHomePageAction, Actions.SetHomePageAction>
| ActionType<typeof skipOnboardingAction, Actions.SkipOnboardingAction>;

export interface State {
usersByLogin: { [login: string]: any };
userLogins: string[];
currentUser: T.CurrentUser;
currentUserSettings: T.CurrentUserSettings;
}

export function receiveCurrentUser(user: T.CurrentUser) {
return { type: 'RECEIVE_CURRENT_USER', user };
return { type: Actions.ReceiveCurrentUser, user };
}

function receiveCurrentUserSettings(userSettings: T.CurrentUserSettingData[]) {
return { type: Actions.ReceiveCurrentUserSettings, userSettings };
}

export function fetchCurrentUserSettings() {
return (dispatch: Dispatch) => {
listUserSettings().then(
({ userSettings }) => dispatch(receiveCurrentUserSettings(userSettings)),
() => {}
);
};
}

export function setCurrentUserSetting(setting: T.CurrentUserSettingData) {
return (dispatch: Dispatch) => {
setUserSetting(setting).then(() => dispatch(receiveCurrentUserSettings([setting])), () => {});
};
}

function skipOnboardingAction() {
return { type: 'SKIP_ONBOARDING' };
return { type: Actions.SkipOnboardingAction };
}

export function skipOnboarding() {
@@ -39,7 +79,7 @@ export function skipOnboarding() {
}

function setHomePageAction(homepage: T.HomePage) {
return { type: 'SET_HOMEPAGE', homepage };
return { type: Actions.SetHomePageAction, homepage };
}

export function setHomePage(homepage: T.HomePage) {
@@ -53,19 +93,8 @@ export function setHomePage(homepage: T.HomePage) {
};
}

type Action =
| ActionType<typeof receiveCurrentUser, 'RECEIVE_CURRENT_USER'>
| ActionType<typeof skipOnboardingAction, 'SKIP_ONBOARDING'>
| ActionType<typeof setHomePageAction, 'SET_HOMEPAGE'>;

export interface State {
usersByLogin: { [login: string]: any };
userLogins: string[];
currentUser: T.CurrentUser;
}

function usersByLogin(state: State['usersByLogin'] = {}, action: Action): State['usersByLogin'] {
if (action.type === 'RECEIVE_CURRENT_USER' && isLoggedIn(action.user)) {
if (action.type === Actions.ReceiveCurrentUser && isLoggedIn(action.user)) {
return { ...state, [action.user.login]: action.user };
} else {
return state;
@@ -73,7 +102,7 @@ function usersByLogin(state: State['usersByLogin'] = {}, action: Action): State[
}

function userLogins(state: State['userLogins'] = [], action: Action): State['userLogins'] {
if (action.type === 'RECEIVE_CURRENT_USER' && isLoggedIn(action.user)) {
if (action.type === Actions.ReceiveCurrentUser && isLoggedIn(action.user)) {
return uniq([...state, action.user.login]);
} else {
return state;
@@ -84,24 +113,42 @@ function currentUser(
state: State['currentUser'] = { isLoggedIn: false },
action: Action
): State['currentUser'] {
if (action.type === 'RECEIVE_CURRENT_USER') {
if (action.type === Actions.ReceiveCurrentUser) {
return action.user;
}
if (action.type === 'SKIP_ONBOARDING' && isLoggedIn(state)) {
if (action.type === Actions.SkipOnboardingAction && isLoggedIn(state)) {
return { ...state, showOnboardingTutorial: false } as T.LoggedInUser;
}
if (action.type === 'SET_HOMEPAGE' && isLoggedIn(state)) {
if (action.type === Actions.SetHomePageAction && isLoggedIn(state)) {
return { ...state, homepage: action.homepage } as T.LoggedInUser;
}
return state;
}

export default combineReducers({ usersByLogin, userLogins, currentUser });
function currentUserSettings(
state: State['currentUserSettings'] = {},
action: Action
): State['currentUserSettings'] {
if (action.type === Actions.ReceiveCurrentUserSettings) {
const newState = { ...state };
action.userSettings.forEach((item: T.CurrentUserSettingData) => {
newState[item.key] = item.value;
});
return newState;
}
return state;
}

export default combineReducers({ usersByLogin, userLogins, currentUser, currentUserSettings });

export function getCurrentUser(state: State) {
return state.currentUser;
}

export function getCurrentUserSettings(state: State) {
return state.currentUserSettings;
}

export function getUserByLogin(state: State, login: string) {
return state.usersByLogin[login];
}

+ 1
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties Datei anzeigen

@@ -107,6 +107,7 @@ my_projects=My Projects
name=Name
navigation=Navigation
never=Never
new=New
new_name=New name
none=None
no_tags=No tags

Laden…
Abbrechen
Speichern