Merge pull request #8336 from strapi/front/application-settings

Add application details page
This commit is contained in:
ELABBASSI Hicham 2020-10-15 17:32:40 +02:00 committed by GitHub
commit e26b45e6be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 619 additions and 179 deletions

View File

@ -1,6 +1,7 @@
import styled from 'styled-components'; import styled from 'styled-components';
const A = styled.a` const A = styled.a`
display: flex;
position: relative; position: relative;
padding-top: 0.7rem; padding-top: 0.7rem;
padding-bottom: 0.2rem; padding-bottom: 0.2rem;
@ -10,7 +11,6 @@ const A = styled.a`
cursor: pointer; cursor: pointer;
color: ${props => props.theme.main.colors.leftMenu['link-color']}; color: ${props => props.theme.main.colors.leftMenu['link-color']};
text-decoration: none; text-decoration: none;
display: block;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
&:hover { &:hover {
@ -31,6 +31,8 @@ const A = styled.a`
} }
&.linkActive { &.linkActive {
padding-right: 2.3rem;
color: white !important; color: white !important;
border-left: 0.3rem solid ${props => props.theme.main.colors.strapi.blue}; border-left: 0.3rem solid ${props => props.theme.main.colors.strapi.blue};
} }

View File

@ -10,10 +10,10 @@ import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import styled from 'styled-components'; import styled from 'styled-components';
import { Link, withRouter } from 'react-router-dom'; import { Link, withRouter } from 'react-router-dom';
import en from '../../../translations/en.json'; import en from '../../../translations/en.json';
import LeftMenuIcon from './LeftMenuIcon'; import LeftMenuIcon from './LeftMenuIcon';
import A from './A'; import A from './A';
import NotificationCount from './NotificationCount';
const LinkLabel = styled.span` const LinkLabel = styled.span`
display: inline-block; display: inline-block;
@ -23,7 +23,7 @@ const LinkLabel = styled.span`
`; `;
// TODO: refacto this file // TODO: refacto this file
const LeftMenuLinkContent = ({ destination, iconName, label, location }) => { const LeftMenuLinkContent = ({ destination, iconName, label, location, notificationsCount }) => {
const isLinkActive = startsWith( const isLinkActive = startsWith(
location.pathname.replace('/admin', '').concat('/'), location.pathname.replace('/admin', '').concat('/'),
destination.concat('/') destination.concat('/')
@ -67,6 +67,7 @@ const LeftMenuLinkContent = ({ destination, iconName, label, location }) => {
> >
<LeftMenuIcon icon={iconName} /> <LeftMenuIcon icon={iconName} />
{content} {content}
{notificationsCount > 0 && <NotificationCount count={notificationsCount} />}
</A> </A>
); );
}; };
@ -78,6 +79,7 @@ LeftMenuLinkContent.propTypes = {
location: PropTypes.shape({ location: PropTypes.shape({
pathname: PropTypes.string, pathname: PropTypes.string,
}).isRequired, }).isRequired,
notificationsCount: PropTypes.number.isRequired,
}; };
export default withRouter(LeftMenuLinkContent); export default withRouter(LeftMenuLinkContent);

View File

@ -0,0 +1,31 @@
import React from 'react';
import styled from 'styled-components';
import PropTypes from 'prop-types';
import { Text } from '@buffetjs/core';
const NotificationWrapper = styled.div`
height: 14px;
margin-top: 4px;
padding: 0px 4px;
background-color: #383d49;
border-radius: 2px;
font-size: 11px;
`;
const NotificationCount = ({ count }) => (
<NotificationWrapper>
<Text fontWeight="bold" fontSize="xs" lineHeight="14px" color="#919bae">
{count}
</Text>
</NotificationWrapper>
);
NotificationCount.defaultProps = {
count: 0,
};
NotificationCount.propTypes = {
count: PropTypes.number,
};
export default NotificationCount;

View File

@ -9,13 +9,14 @@ import PropTypes from 'prop-types';
import LeftMenuLinkContent from './LeftMenuLinkContent'; import LeftMenuLinkContent from './LeftMenuLinkContent';
const LeftMenuLink = ({ destination, iconName, label, location }) => { const LeftMenuLink = ({ destination, iconName, label, location, notificationsCount }) => {
return ( return (
<LeftMenuLinkContent <LeftMenuLinkContent
destination={destination} destination={destination}
iconName={iconName} iconName={iconName}
label={label} label={label}
location={location} location={location}
notificationsCount={notificationsCount}
/> />
); );
}; };
@ -27,6 +28,7 @@ LeftMenuLink.propTypes = {
location: PropTypes.shape({ location: PropTypes.shape({
pathname: PropTypes.string, pathname: PropTypes.string,
}).isRequired, }).isRequired,
notificationsCount: PropTypes.number.isRequired,
}; };
LeftMenuLink.defaultProps = { LeftMenuLink.defaultProps = {

View File

@ -3,7 +3,6 @@ import styled from 'styled-components';
const LeftMenuListLink = styled.div` const LeftMenuListLink = styled.div`
max-height: 180px; max-height: 180px;
margin-bottom: 19px; margin-bottom: 19px;
margin-right: 28px;
overflow: auto; overflow: auto;
`; `;

View File

@ -46,6 +46,7 @@ const LeftMenuLinksSection = ({
iconName={link.icon} iconName={link.icon}
label={link.label} label={link.label}
destination={link.destination} destination={link.destination}
notificationsCount={link.notificationsCount || 0}
/> />
)) ))
) : ( ) : (

View File

@ -5,12 +5,20 @@
*/ */
import { import {
GET_STRAPI_LATEST_RELEASE_SUCCEEDED,
GET_USER_PERMISSIONS, GET_USER_PERMISSIONS,
GET_USER_PERMISSIONS_ERROR, GET_USER_PERMISSIONS_ERROR,
GET_USER_PERMISSIONS_SUCCEEDED, GET_USER_PERMISSIONS_SUCCEEDED,
SET_APP_ERROR, SET_APP_ERROR,
} from './constants'; } from './constants';
export function getStrapiLatestReleaseSucceeded(latestStrapiReleaseTag) {
return {
type: GET_STRAPI_LATEST_RELEASE_SUCCEEDED,
latestStrapiReleaseTag,
};
}
export function getUserPermissions() { export function getUserPermissions() {
return { return {
type: GET_USER_PERMISSIONS, type: GET_USER_PERMISSIONS,

View File

@ -5,6 +5,8 @@
*/ */
export const SET_APP_ERROR = 'StrapiAdmin/Admin/SET_APP_ERROR'; export const SET_APP_ERROR = 'StrapiAdmin/Admin/SET_APP_ERROR';
export const GET_STRAPI_LATEST_RELEASE_SUCCEEDED =
'StrapiAdmin/Admin/GET_STRAPI_LATEST_RELEASE_SUCCEEDED';
export const GET_USER_PERMISSIONS = 'StrapiAdmin/Admin/GET_USER_PERMISSIONS'; export const GET_USER_PERMISSIONS = 'StrapiAdmin/Admin/GET_USER_PERMISSIONS';
export const GET_USER_PERMISSIONS_ERROR = 'StrapiAdmin/Admin/GET_USER_PERMISSIONS_ERROR'; export const GET_USER_PERMISSIONS_ERROR = 'StrapiAdmin/Admin/GET_USER_PERMISSIONS_ERROR';
export const GET_USER_PERMISSIONS_SUCCEEDED = 'StrapiAdmin/Admin/GET_USER_PERMISSIONS_SUCCEEDED'; export const GET_USER_PERMISSIONS_SUCCEEDED = 'StrapiAdmin/Admin/GET_USER_PERMISSIONS_SUCCEEDED';

View File

@ -23,7 +23,7 @@ import {
CheckPagePermissions, CheckPagePermissions,
request, request,
} from 'strapi-helper-plugin'; } from 'strapi-helper-plugin';
import { SETTINGS_BASE_URL, SHOW_TUTORIALS } from '../../config'; import { SETTINGS_BASE_URL, SHOW_TUTORIALS, STRAPI_UPDATE_NOTIF } from '../../config';
import adminPermissions from '../../permissions'; import adminPermissions from '../../permissions';
import Header from '../../components/Header/index'; import Header from '../../components/Header/index';
@ -42,10 +42,12 @@ import Logout from './Logout';
import { import {
disableGlobalOverlayBlocker, disableGlobalOverlayBlocker,
enableGlobalOverlayBlocker, enableGlobalOverlayBlocker,
getInfosDataSucceeded,
updatePlugin, updatePlugin,
} from '../App/actions'; } from '../App/actions';
import makeSelecApp from '../App/selectors'; import makeSelecApp from '../App/selectors';
import { import {
getStrapiLatestReleaseSucceeded,
getUserPermissions, getUserPermissions,
getUserPermissionsError, getUserPermissionsError,
getUserPermissionsSucceeded, getUserPermissionsSucceeded,
@ -67,7 +69,7 @@ export class Admin extends React.Component {
componentDidMount() { componentDidMount() {
this.emitEvent('didAccessAuthenticatedAdministration'); this.emitEvent('didAccessAuthenticatedAdministration');
this.fetchUserPermissions(true); this.initApp();
} }
shouldComponentUpdate(prevProps) { shouldComponentUpdate(prevProps) {
@ -108,6 +110,59 @@ export class Admin extends React.Component {
} }
}; };
fetchAppInfo = async () => {
try {
const { data } = await request('/admin/information', { method: 'GET' });
this.props.getInfosDataSucceeded(data);
} catch (err) {
console.error(err);
strapi.notification.error('notification.error');
}
};
fetchStrapiLatestRelease = async () => {
const {
global: { strapiVersion },
getStrapiLatestReleaseSucceeded,
} = this.props;
if (!STRAPI_UPDATE_NOTIF) {
return;
}
try {
const {
data: { tag_name },
} = await axios.get('https://api.github.com/repos/strapi/strapi/releases/latest');
getStrapiLatestReleaseSucceeded(tag_name);
const showUpdateNotif = !JSON.parse(localStorage.getItem('STRAPI_UPDATE_NOTIF'));
if (!showUpdateNotif) {
return;
}
if (`v${strapiVersion}` !== tag_name) {
strapi.notification.toggle({
type: 'info',
message: { id: 'notification.version.update.message' },
link: {
url: `https://github.com/strapi/strapi/releases/tag/${tag_name}`,
label: {
id: 'notification.version.update.link',
},
},
blockTransition: true,
onClose: () => localStorage.setItem('STRAPI_UPDATE_NOTIF', true),
});
}
} catch (err) {
// Silent
}
};
fetchUserPermissions = async (resetState = false) => { fetchUserPermissions = async (resetState = false) => {
const { getUserPermissions, getUserPermissionsError, getUserPermissionsSucceeded } = this.props; const { getUserPermissions, getUserPermissionsError, getUserPermissionsSucceeded } = this.props;
@ -134,6 +189,12 @@ export class Admin extends React.Component {
return !Object.keys(plugins).every(plugin => plugins[plugin].isReady === true); return !Object.keys(plugins).every(plugin => plugins[plugin].isReady === true);
}; };
initApp = async () => {
await this.fetchAppInfo();
await this.fetchStrapiLatestRelease();
await this.fetchUserPermissions(true);
};
/** /**
* Display the app loader until the app is ready * Display the app loader until the app is ready
* @returns {Boolean} * @returns {Boolean}
@ -170,7 +231,7 @@ export class Admin extends React.Component {
render() { render() {
const { const {
admin: { isLoading, userPermissions }, admin: { isLoading, latestStrapiReleaseTag, userPermissions },
global: { global: {
autoReload, autoReload,
blockApp, blockApp,
@ -211,14 +272,21 @@ export class Admin extends React.Component {
enableGlobalOverlayBlocker={enableGlobalOverlayBlocker} enableGlobalOverlayBlocker={enableGlobalOverlayBlocker}
fetchUserPermissions={this.fetchUserPermissions} fetchUserPermissions={this.fetchUserPermissions}
formatMessage={formatMessage} formatMessage={formatMessage}
latestStrapiReleaseTag={latestStrapiReleaseTag}
menu={this.menuRef.current} menu={this.menuRef.current}
plugins={plugins} plugins={plugins}
settingsBaseURL={SETTINGS_BASE_URL || '/settings'} settingsBaseURL={SETTINGS_BASE_URL || '/settings'}
strapiVersion={strapiVersion}
updatePlugin={updatePlugin} updatePlugin={updatePlugin}
> >
<UserProvider value={userPermissions}> <UserProvider value={userPermissions}>
<Wrapper> <Wrapper>
<LeftMenu version={strapiVersion} plugins={plugins} ref={this.menuRef} /> <LeftMenu
latestStrapiReleaseTag={latestStrapiReleaseTag}
version={strapiVersion}
plugins={plugins}
ref={this.menuRef}
/>
<NavTopRightWrapper> <NavTopRightWrapper>
{/* Injection zone not ready yet */} {/* Injection zone not ready yet */}
<Logout /> <Logout />
@ -275,10 +343,13 @@ Admin.propTypes = {
admin: PropTypes.shape({ admin: PropTypes.shape({
appError: PropTypes.bool, appError: PropTypes.bool,
isLoading: PropTypes.bool, isLoading: PropTypes.bool,
latestStrapiReleaseTag: PropTypes.string.isRequired,
userPermissions: PropTypes.array, userPermissions: PropTypes.array,
}).isRequired, }).isRequired,
disableGlobalOverlayBlocker: PropTypes.func.isRequired, disableGlobalOverlayBlocker: PropTypes.func.isRequired,
enableGlobalOverlayBlocker: PropTypes.func.isRequired, enableGlobalOverlayBlocker: PropTypes.func.isRequired,
getInfosDataSucceeded: PropTypes.func.isRequired,
getStrapiLatestReleaseSucceeded: PropTypes.func.isRequired,
getUserPermissions: PropTypes.func.isRequired, getUserPermissions: PropTypes.func.isRequired,
getUserPermissionsError: PropTypes.func.isRequired, getUserPermissionsError: PropTypes.func.isRequired,
getUserPermissionsSucceeded: PropTypes.func.isRequired, getUserPermissionsSucceeded: PropTypes.func.isRequired,
@ -311,6 +382,8 @@ export function mapDispatchToProps(dispatch) {
{ {
disableGlobalOverlayBlocker, disableGlobalOverlayBlocker,
enableGlobalOverlayBlocker, enableGlobalOverlayBlocker,
getInfosDataSucceeded,
getStrapiLatestReleaseSucceeded,
getUserPermissions, getUserPermissions,
getUserPermissionsError, getUserPermissionsError,
getUserPermissionsSucceeded, getUserPermissionsSucceeded,

View File

@ -5,17 +5,21 @@
*/ */
import produce from 'immer'; import produce from 'immer';
import packageJSON from '../../../../package.json';
import { import {
GET_STRAPI_LATEST_RELEASE_SUCCEEDED,
GET_USER_PERMISSIONS, GET_USER_PERMISSIONS,
GET_USER_PERMISSIONS_ERROR, GET_USER_PERMISSIONS_ERROR,
GET_USER_PERMISSIONS_SUCCEEDED, GET_USER_PERMISSIONS_SUCCEEDED,
SET_APP_ERROR, SET_APP_ERROR,
} from './constants'; } from './constants';
const packageVersion = packageJSON.version;
const initialState = { const initialState = {
appError: false, appError: false,
isLoading: true, isLoading: true,
latestStrapiReleaseTag: `v${packageVersion}`,
userPermissions: [], userPermissions: [],
}; };
@ -23,6 +27,10 @@ const reducer = (state = initialState, action) =>
// eslint-disable-next-line consistent-return // eslint-disable-next-line consistent-return
produce(state, draftState => { produce(state, draftState => {
switch (action.type) { switch (action.type) {
case GET_STRAPI_LATEST_RELEASE_SUCCEEDED: {
draftState.latestStrapiReleaseTag = action.latestStrapiReleaseTag;
break;
}
case GET_USER_PERMISSIONS: { case GET_USER_PERMISSIONS: {
draftState.isLoading = true; draftState.isLoading = true;
break; break;

View File

@ -19,10 +19,13 @@ describe('<Admin />', () => {
props = { props = {
admin: { admin: {
appError: false, appError: false,
latestStrapiReleaseTag: '3',
}, },
disableGlobalOverlayBlocker: jest.fn(), disableGlobalOverlayBlocker: jest.fn(),
emitEvent: jest.fn(), emitEvent: jest.fn(),
enableGlobalOverlayBlocker: jest.fn(), enableGlobalOverlayBlocker: jest.fn(),
getInfosDataSucceeded: jest.fn(),
getStrapiLatestReleaseSucceeded: jest.fn(),
getUserPermissions: jest.fn(), getUserPermissions: jest.fn(),
getUserPermissionsError: jest.fn(), getUserPermissionsError: jest.fn(),
getUserPermissionsSucceeded: jest.fn(), getUserPermissionsSucceeded: jest.fn(),
@ -42,6 +45,7 @@ describe('<Admin />', () => {
intl: { intl: {
formatMessage: jest.fn(), formatMessage: jest.fn(),
}, },
location: {}, location: {},
setAppError: jest.fn(), setAppError: jest.fn(),
showGlobalAppBlocker: jest.fn(), showGlobalAppBlocker: jest.fn(),

View File

@ -1,4 +1,5 @@
import produce from 'immer'; import produce from 'immer';
import packageJSON from '../../../../../package.json';
import { import {
setAppError, setAppError,
getUserPermissions, getUserPermissions,
@ -14,6 +15,7 @@ describe('adminReducer', () => {
state = { state = {
appError: false, appError: false,
isLoading: true, isLoading: true,
latestStrapiReleaseTag: `v${packageJSON.version}`,
userPermissions: [], userPermissions: [],
}; };
}); });

View File

@ -8,12 +8,12 @@ import {
DISABLE_GLOBAL_OVERLAY_BLOCKER, DISABLE_GLOBAL_OVERLAY_BLOCKER,
ENABLE_GLOBAL_OVERLAY_BLOCKER, ENABLE_GLOBAL_OVERLAY_BLOCKER,
FREEZE_APP, FREEZE_APP,
GET_INFOS_DATA_SUCCEEDED,
GET_DATA_SUCCEEDED, GET_DATA_SUCCEEDED,
LOAD_PLUGIN, LOAD_PLUGIN,
PLUGIN_DELETED, PLUGIN_DELETED,
PLUGIN_LOADED, PLUGIN_LOADED,
UNFREEZE_APP, UNFREEZE_APP,
UNSET_HAS_USERS_PLUGIN,
UPDATE_PLUGIN, UPDATE_PLUGIN,
} from './constants'; } from './constants';
@ -36,6 +36,13 @@ export function freezeApp(data) {
}; };
} }
export function getInfosDataSucceeded(data) {
return {
type: GET_INFOS_DATA_SUCCEEDED,
data,
};
}
export function getDataSucceeded(data) { export function getDataSucceeded(data) {
return { return {
type: GET_DATA_SUCCEEDED, type: GET_DATA_SUCCEEDED,
@ -70,12 +77,6 @@ export function unfreezeApp() {
}; };
} }
export function unsetHasUserPlugin() {
return {
type: UNSET_HAS_USERS_PLUGIN,
};
}
export function updatePlugin(pluginId, updatedKey, updatedValue) { export function updatePlugin(pluginId, updatedKey, updatedValue) {
return { return {
type: UPDATE_PLUGIN, type: UPDATE_PLUGIN,

View File

@ -9,9 +9,10 @@ export const LOAD_PLUGIN = 'app/App/LOAD_PLUGIN';
export const PLUGIN_LOADED = 'app/App/PLUGIN_LOADED'; export const PLUGIN_LOADED = 'app/App/PLUGIN_LOADED';
export const PLUGIN_DELETED = 'app/App/PLUGIN_DELETED'; export const PLUGIN_DELETED = 'app/App/PLUGIN_DELETED';
export const UNFREEZE_APP = 'app/App/UNFREEZE_APP'; export const UNFREEZE_APP = 'app/App/UNFREEZE_APP';
export const UNSET_HAS_USERS_PLUGIN = 'app/App/UNSET_HAS_USERS_PLUGIN';
export const UPDATE_PLUGIN = 'app/App/UPDATE_PLUGIN'; export const UPDATE_PLUGIN = 'app/App/UPDATE_PLUGIN';
export const DISABLE_GLOBAL_OVERLAY_BLOCKER = export const DISABLE_GLOBAL_OVERLAY_BLOCKER =
'app/App/OverlayBlocker/DISABLE_GLOBAL_OVERLAY_BLOCKER'; 'app/App/OverlayBlocker/DISABLE_GLOBAL_OVERLAY_BLOCKER';
export const ENABLE_GLOBAL_OVERLAY_BLOCKER = 'app/App/OverlayBlocker/ENABLE_GLOBAL_OVERLAY_BLOCKER'; export const ENABLE_GLOBAL_OVERLAY_BLOCKER = 'app/App/OverlayBlocker/ENABLE_GLOBAL_OVERLAY_BLOCKER';
export const GET_DATA_SUCCEEDED = 'app/App/GET_DATA_SUCCEEDED'; export const GET_DATA_SUCCEEDED = 'app/App/GET_DATA_SUCCEEDED';
export const GET_INFOS_DATA_SUCCEEDED = 'admin/App/GET_INFOS_DATA_SUCCEEDED';

View File

@ -35,25 +35,31 @@ function App(props) {
getDataRef.current = props.getDataSucceeded; getDataRef.current = props.getDataSucceeded;
useEffect(() => { useEffect(() => {
const getData = async () => { const currentToken = auth.getToken();
const currentToken = auth.getToken();
if (currentToken) { const renewToken = async () => {
try { try {
const { const {
data: { token }, data: { token },
} = await request('/admin/renew-token', { } = await request('/admin/renew-token', {
method: 'POST', method: 'POST',
body: { token: currentToken }, body: { token: currentToken },
}); });
auth.updateToken(token); auth.updateToken(token);
} catch (err) { } catch (err) {
// Refresh app // Refresh app
auth.clearAppStorage(); auth.clearAppStorage();
window.location.reload(); window.location.reload();
}
} }
};
if (currentToken) {
renewToken();
}
}, []);
useEffect(() => {
const getData = async () => {
try { try {
const { data } = await request('/admin/init', { method: 'GET' }); const { data } = await request('/admin/init', { method: 'GET' });
@ -87,7 +93,7 @@ function App(props) {
}; };
getData(); getData();
}, [getDataRef]); }, []);
if (isLoading) { if (isLoading) {
return <LoadingIndicatorPage />; return <LoadingIndicatorPage />;

View File

@ -6,16 +6,17 @@ import {
DISABLE_GLOBAL_OVERLAY_BLOCKER, DISABLE_GLOBAL_OVERLAY_BLOCKER,
ENABLE_GLOBAL_OVERLAY_BLOCKER, ENABLE_GLOBAL_OVERLAY_BLOCKER,
FREEZE_APP, FREEZE_APP,
GET_INFOS_DATA_SUCCEEDED,
GET_DATA_SUCCEEDED, GET_DATA_SUCCEEDED,
PLUGIN_DELETED, PLUGIN_DELETED,
PLUGIN_LOADED, PLUGIN_LOADED,
UNFREEZE_APP, UNFREEZE_APP,
UNSET_HAS_USERS_PLUGIN,
UPDATE_PLUGIN, UPDATE_PLUGIN,
} from './constants'; } from './constants';
const packageVersion = packageJSON.version; const packageVersion = packageJSON.version;
const initialState = fromJS({ const initialState = fromJS({
appInfos: {},
autoReload: false, autoReload: false,
blockApp: false, blockApp: false,
currentEnvironment: 'development', currentEnvironment: 'development',
@ -43,24 +44,27 @@ function appReducer(state = initialState, action) {
return null; return null;
}); });
case GET_DATA_SUCCEEDED: { case GET_INFOS_DATA_SUCCEEDED: {
const { if (action.data.strapiVersion !== state.get('strapiVersion')) {
data: { hasAdmin, uuid, currentEnvironment, autoReload, strapiVersion },
} = action;
if (strapiVersion !== state.get('strapiVersion')) {
console.error( console.error(
`It seems that the built version ${packageVersion} is different than your project's one (${strapiVersion})` `It seems that the built version ${packageVersion} is different than your project's one (${action.data.strapiVersion})`
); );
console.error('Please delete your `.cache` and `build` folders and restart your app'); console.error('Please delete your `.cache` and `build` folders and restart your app');
} }
return (
state
.update('appInfos', () => action.data)
// Keep this for plugins legacy
.update('autoReload', () => action.data.autoReload)
.update('currentEnvironment', () => action.data.currentEnvironment)
);
}
case GET_DATA_SUCCEEDED: {
return state return state
.update('isLoading', () => false) .update('isLoading', () => false)
.update('hasAdminUser', () => hasAdmin) .update('hasAdminUser', () => action.data.hasAdmin)
.update('uuid', () => uuid) .update('uuid', () => action.data.uuid);
.update('autoReload', () => autoReload)
.update('currentEnvironment', () => currentEnvironment);
} }
case PLUGIN_LOADED: case PLUGIN_LOADED:
return state.setIn(['plugins', action.plugin.id], fromJS(action.plugin)); return state.setIn(['plugins', action.plugin.id], fromJS(action.plugin));
@ -73,8 +77,7 @@ function appReducer(state = initialState, action) {
return state.deleteIn(['plugins', action.plugin]); return state.deleteIn(['plugins', action.plugin]);
case UNFREEZE_APP: case UNFREEZE_APP:
return state.set('blockApp', false).set('overlayBlockerData', null); return state.set('blockApp', false).set('overlayBlockerData', null);
case UNSET_HAS_USERS_PLUGIN:
return state.set('hasUserPlugin', false);
default: default:
return state; return state;
} }

View File

@ -9,47 +9,22 @@ const selectApp = () => state => state.get('app');
* Select the language locale * Select the language locale
*/ */
const selectPlugins = () => const selectPlugins = () => createSelector(selectApp(), appState => appState.get('plugins'));
createSelector(
selectApp(),
appState => appState.get('plugins')
);
const makeSelectApp = () => const makeSelectApp = () => createSelector(selectApp(), appState => appState.toJS());
createSelector(
selectApp(),
appState => appState.toJS()
);
const selectHasUserPlugin = () => const selectHasUserPlugin = () =>
createSelector( createSelector(selectApp(), appState => appState.get('hasUserPlugin'));
selectApp(),
appState => appState.get('hasUserPlugin')
);
const makeSelectShowGlobalAppBlocker = () => const makeSelectShowGlobalAppBlocker = () =>
createSelector( createSelector(selectApp(), appState => appState.get('showGlobalAppBlocker'));
selectApp(),
appState => appState.get('showGlobalAppBlocker')
);
const makeSelectBlockApp = () => const makeSelectBlockApp = () => createSelector(selectApp(), appState => appState.get('blockApp'));
createSelector(
selectApp(),
appState => appState.get('blockApp')
);
const makeSelectOverlayBlockerProps = () => const makeSelectOverlayBlockerProps = () =>
createSelector( createSelector(selectApp(), appState => appState.get('overlayBlockerData'));
selectApp(),
appState => appState.get('overlayBlockerData')
);
const makeSelectUuid = () => const makeSelectUuid = () => createSelector(selectApp(), appState => appState.get('uuid'));
createSelector(
selectApp(),
appState => appState.get('uuid')
);
export default makeSelectApp; export default makeSelectApp;
export { export {

View File

@ -1,19 +1,21 @@
import { import {
FREEZE_APP, FREEZE_APP,
GET_DATA_SUCCEEDED,
GET_INFOS_DATA_SUCCEEDED,
LOAD_PLUGIN, LOAD_PLUGIN,
PLUGIN_DELETED, PLUGIN_DELETED,
PLUGIN_LOADED, PLUGIN_LOADED,
UNFREEZE_APP, UNFREEZE_APP,
UNSET_HAS_USERS_PLUGIN,
UPDATE_PLUGIN, UPDATE_PLUGIN,
} from '../constants'; } from '../constants';
import { import {
freezeApp, freezeApp,
loadPlugin, loadPlugin,
getInfosDataSucceeded,
getDataSucceeded,
pluginDeleted, pluginDeleted,
pluginLoaded, pluginLoaded,
unfreezeApp, unfreezeApp,
unsetHasUserPlugin,
updatePlugin, updatePlugin,
} from '../actions'; } from '../actions';
@ -40,6 +42,30 @@ describe('<App /> actions', () => {
}); });
}); });
describe('getDataSucceeded', () => {
it('shoudl return the correct type and the passed data', () => {
const data = { ok: true };
const expected = {
type: GET_DATA_SUCCEEDED,
data,
};
expect(getDataSucceeded(data)).toEqual(expected);
});
});
describe('getInfosDataSucceeded', () => {
it('shoudl return the correct type and the passed data', () => {
const data = { ok: true };
const expected = {
type: GET_INFOS_DATA_SUCCEEDED,
data,
};
expect(getInfosDataSucceeded(data)).toEqual(expected);
});
});
describe('loadPlugin', () => { describe('loadPlugin', () => {
it('should return the correct type and the passed data', () => { it('should return the correct type and the passed data', () => {
const plugin = { const plugin = {
@ -82,16 +108,6 @@ describe('<App /> actions', () => {
}); });
}); });
describe('unsetHasUserPlugin', () => {
it('should return the correct type', () => {
const expected = {
type: UNSET_HAS_USERS_PLUGIN,
};
expect(unsetHasUserPlugin()).toEqual(expected);
});
});
describe('updatePlugin', () => { describe('updatePlugin', () => {
it('should return the correct type and the passed data', () => { it('should return the correct type and the passed data', () => {
const pluginId = 'content-manager'; const pluginId = 'content-manager';
@ -104,9 +120,7 @@ describe('<App /> actions', () => {
updatedValue, updatedValue,
}; };
expect(updatePlugin(pluginId, updatedKey, updatedValue)).toEqual( expect(updatePlugin(pluginId, updatedKey, updatedValue)).toEqual(expected);
expected
);
}); });
}); });
}); });

View File

@ -4,10 +4,11 @@ import {
disableGlobalOverlayBlocker, disableGlobalOverlayBlocker,
enableGlobalOverlayBlocker, enableGlobalOverlayBlocker,
freezeApp, freezeApp,
getDataSucceeded,
getInfosDataSucceeded,
pluginDeleted, pluginDeleted,
pluginLoaded, pluginLoaded,
unfreezeApp, unfreezeApp,
unsetHasUserPlugin,
updatePlugin, updatePlugin,
} from '../actions'; } from '../actions';
import appReducer from '../reducer'; import appReducer from '../reducer';
@ -17,6 +18,7 @@ describe('<App /> reducer', () => {
beforeEach(() => { beforeEach(() => {
state = fromJS({ state = fromJS({
appInfos: {},
autoReload: false, autoReload: false,
blockApp: false, blockApp: false,
currentEnvironment: 'development', currentEnvironment: 'development',
@ -96,9 +98,34 @@ describe('<App /> reducer', () => {
expect(appReducer(state, unfreezeApp())).toEqual(expectedResult); expect(appReducer(state, unfreezeApp())).toEqual(expectedResult);
}); });
it('should handle the unsetHasUserPlugin action correclty', () => { describe('GET_INFOS_DATA_SUCCEEDED', () => {
const expectedResult = state.set('hasUserPlugin', false); it('should handle the set the data correctly', () => {
const data = {
autoReload: true,
communityEdition: false,
currentEnvironment: 'test',
nodeVersion: 'v12.14.1',
strapiVersion: '3.2.1',
};
const expected = state
.set('appInfos', data)
.set('autoReload', true)
.set('currentEnvironment', 'test');
expect(appReducer(state, unsetHasUserPlugin())).toEqual(expectedResult); expect(appReducer(state, getInfosDataSucceeded(data))).toEqual(expected);
});
});
describe('GET_DATA_SUCCEEDED', () => {
it('should handle the set the data correctly', () => {
const expected = state
.set('hasAdminUser', true)
.set('uuid', 'true')
.set('isLoading', false);
expect(appReducer(state, getDataSucceeded({ hasAdmin: true, uuid: 'true' }))).toEqual(
expected
);
});
}); });
}); });

View File

@ -0,0 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Text } from '@buffetjs/core';
import InfoText from '../InfoText';
import Link from '../Link';
import Wrapper from '../Wrapper';
const Detail = ({ content, link, title }) => {
return (
<Wrapper>
<Text fontSize="xs" color="grey" fontWeight="bold">
{title}
</Text>
<InfoText content={content} />
{link && <Link {...link} />}
</Wrapper>
);
};
Detail.defaultProps = {
link: null,
};
Detail.propTypes = {
content: PropTypes.string.isRequired,
link: PropTypes.object,
title: PropTypes.string.isRequired,
};
export default Detail;

View File

@ -0,0 +1,19 @@
import React from 'react';
import { Padded, Text } from '@buffetjs/core';
import PropTypes from 'prop-types';
const InfoText = ({ content }) => {
return (
<Padded top size="xs">
<Text fontWeight="semiBold" lineHeight="13px">
{content}
</Text>
</Padded>
);
};
InfoText.propTypes = {
content: PropTypes.string.isRequired,
};
export default InfoText;

View File

@ -0,0 +1,22 @@
import styled from 'styled-components';
import { Text } from '@buffetjs/core';
import { Arrow } from '@buffetjs/icons';
const LinkText = styled(Text)`
color: ${({ theme }) => theme.main.colors.mediumBlue};
> a {
&:hover {
color: ${({ theme }) => theme.main.colors.mediumBlue};
text-decoration: none;
}
}
`;
export const LinkArrow = styled(Arrow)`
transform: rotate(45deg);
margin-top: 2px;
margin-left: 10px;
color: ${({ theme }) => theme.main.colors.blue};
`;
export default LinkText;

View File

@ -0,0 +1,26 @@
import React from 'react';
import { Padded } from '@buffetjs/core';
import PropTypes from 'prop-types';
import BaselineAlignement from '../../../../components/BaselineAlignement';
import LinkText, { LinkArrow } from './components';
const Link = ({ href, label }) => {
return (
<Padded top size="smd">
<BaselineAlignement top size="1px" />
<LinkText fontWeight="semiBold">
<a href={href} target="_blank" rel="noopener noreferrer">
{label}
<LinkArrow />
</a>
</LinkText>
</Padded>
);
};
Link.propTypes = {
href: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
};
export default Link;

View File

@ -0,0 +1,7 @@
import styled from 'styled-components';
const Wrapper = styled.div`
width: 50%;
`;
export default Wrapper;

View File

@ -0,0 +1,4 @@
export { default as Detail } from './Detail';
export { default as InfoText } from './InfoText';
export { default as Link } from './Link';
export { default as Wrapper } from './Wrapper';

View File

@ -0,0 +1,86 @@
import React, { memo, useMemo } from 'react';
import { Header } from '@buffetjs/custom';
import { Flex, Padded, Text } from '@buffetjs/core';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import { useIntl } from 'react-intl';
import BaselineAlignement from '../../components/BaselineAlignement';
import Bloc from '../../components/Bloc';
import PageTitle from '../../components/SettingsPageTitle';
import makeSelectApp from '../App/selectors';
import makeSelectAdmin from '../Admin/selectors';
import { Detail, InfoText } from './components';
const makeSelectAppInfos = () => createSelector(makeSelectApp(), appState => appState.appInfos);
const makeSelectLatestRelease = () =>
createSelector(makeSelectAdmin(), adminState => adminState.latestStrapiReleaseTag);
const ApplicationInfosPage = () => {
const { formatMessage } = useIntl();
const selectAppInfos = useMemo(makeSelectAppInfos, []);
const selectLatestRealase = useMemo(makeSelectLatestRelease, []);
const appInfos = useSelector(state => selectAppInfos(state));
const latestStrapiReleaseTag = useSelector(state => selectLatestRealase(state));
const currentPlan = appInfos.communityEdition
? 'app.components.UpgradePlanModal.text-ce'
: 'app.components.UpgradePlanModal.text-ee';
const headerProps = {
title: { label: formatMessage({ id: 'Settings.application.title' }) },
content: formatMessage({
id: 'Settings.application.description',
}),
};
const pricingLabel = formatMessage({ id: 'Settings.application.link-pricing' });
const upgradeLabel = formatMessage({ id: 'Settings.application.link-upgrade' });
const strapiVersion = formatMessage({ id: 'Settings.application.strapi-version' });
const nodeVersion = formatMessage({ id: 'Settings.application.node-version' });
const editionTitle = formatMessage({ id: 'Settings.application.edition-title' });
const shouldShowUpgradeLink = `v${appInfos.strapiVersion}` !== latestStrapiReleaseTag;
/* eslint-disable indent */
const upgradeLink = shouldShowUpgradeLink
? {
label: upgradeLabel,
href: `https://github.com/strapi/strapi/releases/tag/${latestStrapiReleaseTag}`,
}
: null;
/* eslint-enable indent */
return (
<div>
<PageTitle name="Infos" />
<Header {...headerProps} />
<BaselineAlignement top size="3px" />
<Bloc>
<Padded left right top size="smd">
<Padded left right top size="xs">
<Flex justifyContent="space-between">
<Detail
link={upgradeLink}
title={strapiVersion}
content={`v${appInfos.strapiVersion}`}
/>
<Detail
link={{ label: pricingLabel, href: 'https://strapi.io/pricing' }}
title={editionTitle}
content={formatMessage({ id: currentPlan })}
/>
</Flex>
<Padded top size="lg">
<Text fontSize="xs" color="grey" fontWeight="bold">
{nodeVersion}
</Text>
<InfoText content={appInfos.nodeVersion} />
</Padded>
</Padded>
</Padded>
<BaselineAlignement top size="60px" />
</Bloc>
</div>
);
};
export default memo(ApplicationInfosPage);

View File

@ -4,16 +4,12 @@
* *
*/ */
/* eslint-disable */ /* eslint-disable */
import React, { memo, useMemo, useEffect } from 'react'; import React, { memo, useMemo } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { get, isNil, upperFirst } from 'lodash'; import { get, upperFirst } from 'lodash';
import { auth, LoadingIndicatorPage } from 'strapi-helper-plugin'; import { auth, LoadingIndicatorPage } from 'strapi-helper-plugin';
import axios from 'axios';
import { useSelector } from 'react-redux';
import PageTitle from '../../components/PageTitle'; import PageTitle from '../../components/PageTitle';
import { useModels } from '../../hooks'; import { useModels } from '../../hooks';
import { STRAPI_UPDATE_NOTIF } from '../../config';
import useFetch from './hooks'; import useFetch from './hooks';
import { ALink, Block, Container, LinkWrapper, P, Wave, Separator } from './components'; import { ALink, Block, Container, LinkWrapper, P, Wave, Separator } from './components';
@ -65,54 +61,7 @@ const SOCIAL_LINKS = [
}, },
]; ];
const HomePage = ({ global: { strapiVersion }, history: { push } }) => { const HomePage = ({ history: { push } }) => {
const notifications = useSelector(state => state.get('newNotification').notifications);
useEffect(() => {
const getStrapiLatestRelease = async () => {
try {
const notificationAlreadyExist =
notifications.findIndex(notification => notification.uid === 'STRAPI_UPDATE_NOTIF') != -1;
const showUpdateNotif =
STRAPI_UPDATE_NOTIF &&
!JSON.parse(localStorage.getItem('STRAPI_UPDATE_NOTIF')) &&
!notificationAlreadyExist;
if (showUpdateNotif) {
const res = await fetch('https://api.github.com/repos/strapi/strapi/releases/latest');
const data = await res.json();
if (strapiVersion !== data.name.split('v').join('')) {
strapi.notification.toggle({
type: 'info',
message: { id: 'notification.version.update.message' },
link: {
url: `https://github.com/strapi/strapi/releases/tag/${data.name}`,
label: {
id: 'notification.version.update.link',
},
},
blockTransition: true,
// Used to check if the notification is already displayed
// to avoid multiple notifications each time the user goes back to the home page.
uid: 'STRAPI_UPDATE_NOTIF',
onClose: () => localStorage.setItem('STRAPI_UPDATE_NOTIF', true),
});
}
}
} catch (e) {
strapi.notification.toggle({
type: 'warning',
message: { id: 'notification.error' },
});
}
};
getStrapiLatestRelease();
}, []);
const { error, isLoading, posts } = useFetch(); const { error, isLoading, posts } = useFetch();
// Temporary until we develop the menu API // Temporary until we develop the menu API
const { collectionTypes, singleTypes, isLoading: isLoadingForModels } = useModels(); const { collectionTypes, singleTypes, isLoading: isLoadingForModels } = useModels();

View File

@ -30,10 +30,19 @@ import reducer, { initialState } from './reducer';
import Loader from './Loader'; import Loader from './Loader';
import Wrapper from './Wrapper'; import Wrapper from './Wrapper';
const LeftMenu = forwardRef(({ version, plugins }, ref) => { const LeftMenu = forwardRef(({ latestStrapiReleaseTag, version, plugins }, ref) => {
const location = useLocation(); const location = useLocation();
const permissions = useContext(UserContext); const permissions = useContext(UserContext);
const { menu: settingsMenu } = useSettingsMenu(true); const { menu: settingsMenu } = useSettingsMenu(true);
// TODO: this needs to be added to the settings API in the v4
const settingsLinkNotificationCount = useMemo(() => {
if (`v${version}` !== latestStrapiReleaseTag) {
return 1;
}
return 0;
}, [latestStrapiReleaseTag, version]);
const [ const [
{ {
collectionTypesSectionLinks, collectionTypesSectionLinks,
@ -43,7 +52,9 @@ const LeftMenu = forwardRef(({ version, plugins }, ref) => {
singleTypesSectionLinks, singleTypesSectionLinks,
}, },
dispatch, dispatch,
] = useReducer(reducer, initialState, () => init(initialState, plugins, settingsMenu)); ] = useReducer(reducer, initialState, () =>
init(initialState, plugins, settingsMenu, settingsLinkNotificationCount)
);
const generalSectionLinksFiltered = useMemo(() => filterLinks(generalSectionLinks), [ const generalSectionLinksFiltered = useMemo(() => filterLinks(generalSectionLinks), [
generalSectionLinks, generalSectionLinks,
]); ]);
@ -197,6 +208,7 @@ const LeftMenu = forwardRef(({ version, plugins }, ref) => {
}); });
LeftMenu.propTypes = { LeftMenu.propTypes = {
latestStrapiReleaseTag: PropTypes.string.isRequired,
version: PropTypes.string.isRequired, version: PropTypes.string.isRequired,
plugins: PropTypes.object.isRequired, plugins: PropTypes.object.isRequired,
}; };

View File

@ -3,7 +3,7 @@ import { SETTINGS_BASE_URL } from '../../config';
import { sortLinks } from '../../utils'; import { sortLinks } from '../../utils';
import { getSettingsMenuLinksPermissions } from './utils'; import { getSettingsMenuLinksPermissions } from './utils';
const init = (initialState, plugins = {}, settingsMenu = []) => { const init = (initialState, plugins = {}, settingsMenu = [], settingsLinkNotificationCount = 0) => {
const settingsLinkPermissions = getSettingsMenuLinksPermissions(settingsMenu); const settingsLinkPermissions = getSettingsMenuLinksPermissions(settingsMenu);
const pluginsLinks = Object.values(plugins).reduce((acc, current) => { const pluginsLinks = Object.values(plugins).reduce((acc, current) => {
@ -21,8 +21,10 @@ const init = (initialState, plugins = {}, settingsMenu = []) => {
if (!settingsLinkPermissions.filter(perm => perm === null).length && settingsLinkIndex !== -1) { if (!settingsLinkPermissions.filter(perm => perm === null).length && settingsLinkIndex !== -1) {
const permissionsPath = ['generalSectionLinks', settingsLinkIndex, 'permissions']; const permissionsPath = ['generalSectionLinks', settingsLinkIndex, 'permissions'];
const notificationPath = ['generalSectionLinks', settingsLinkIndex, 'notificationsCount'];
set(initialState, permissionsPath, settingsLinkPermissions); set(initialState, permissionsPath, settingsLinkPermissions);
set(initialState, notificationPath, settingsLinkNotificationCount);
} }
if (sortedLinks.length) { if (sortedLinks.length) {

View File

@ -13,6 +13,7 @@ const initialState = {
destination: '/list-plugins', destination: '/list-plugins',
isDisplayed: false, isDisplayed: false,
permissions: adminPermissions.marketplace.main, permissions: adminPermissions.marketplace.main,
notificationsCount: 0,
}, },
{ {
icon: 'shopping-basket', icon: 'shopping-basket',
@ -20,6 +21,7 @@ const initialState = {
destination: '/marketplace', destination: '/marketplace',
isDisplayed: false, isDisplayed: false,
permissions: adminPermissions.marketplace.main, permissions: adminPermissions.marketplace.main,
notificationsCount: 0,
}, },
{ {
icon: 'cog', icon: 'cog',
@ -29,6 +31,7 @@ const initialState = {
// Permissions of this link are retrieved in the init phase // Permissions of this link are retrieved in the init phase
// using the settings menu // using the settings menu
permissions: [], permissions: [],
notificationsCount: 0,
}, },
], ],
singleTypesSectionLinks: [], singleTypesSectionLinks: [],

View File

@ -112,6 +112,7 @@ describe('ADMIN | LeftMenu | init', () => {
title: 'Settings.webhooks.title', title: 'Settings.webhooks.title',
to: '/settings/webhooks', to: '/settings/webhooks',
name: 'webhooks', name: 'webhooks',
permissions: [ permissions: [
{ action: 'admin::webhook.create', subject: null }, { action: 'admin::webhook.create', subject: null },
{ action: 'admin::webhook.read', subject: null }, { action: 'admin::webhook.read', subject: null },
@ -223,6 +224,7 @@ describe('ADMIN | LeftMenu | init', () => {
label: 'app.components.LeftMenuLinkContainer.settings', label: 'app.components.LeftMenuLinkContainer.settings',
isDisplayed: false, isDisplayed: false,
destination: SETTINGS_BASE_URL, destination: SETTINGS_BASE_URL,
notificationsCount: 0,
permissions: [ permissions: [
// webhooks // webhooks
{ action: 'admin::webhook.create', subject: null }, { action: 'admin::webhook.create', subject: null },

View File

@ -0,0 +1,13 @@
import React from 'react';
import styled from 'styled-components';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
const LeftMenuIcon = styled(({ ...props }) => <FontAwesomeIcon {...props} icon="circle" />)`
position: absolute;
top: calc(50% - 0.25rem);
left: 1.5rem;
font-size: 0.5rem;
color: ${props => props.theme.main.colors.leftMenu['link-color']};
`;
export default LeftMenuIcon;

View File

@ -0,0 +1,25 @@
import styled from 'styled-components';
import { NavLink } from 'react-router-dom';
const Link = styled(NavLink)`
display: flex;
justify-content: space-between;
position: relative;
padding-left: 30px;
height: 34px;
border-radius: 2px;
&.active {
background-color: #e9eaeb;
> p {
font-weight: 600;
}
> svg {
color: #2d3138;
}
}
&:hover {
text-decoration: none;
}
`;
export default Link;

View File

@ -0,0 +1,17 @@
import styled from 'styled-components';
const Notif = styled.div`
margin: auto;
margin-right: 15px;
&:before {
content: '';
display: flex;
width: 6px;
height: 6px;
border-radius: 50%;
background-color: #007dff;
}
`;
export default Notif;

View File

@ -0,0 +1,9 @@
import styled from 'styled-components';
const Wrapper = styled.div`
position: relative;
margin-bottom: -5px;
padding: 25px 20px 0 20px;
`;
export default Wrapper;

View File

@ -0,0 +1,27 @@
import React, { memo } from 'react';
import { Text } from '@buffetjs/core';
import { FormattedMessage } from 'react-intl';
import { useGlobalContext } from 'strapi-helper-plugin';
import Icon from './Icon';
import Link from './Link';
import Notif from './Notif';
import Wrapper from './Wrapper';
const ApplicationDetailLink = () => {
const { latestStrapiReleaseTag, strapiVersion } = useGlobalContext();
const showNotif = `v${strapiVersion}` !== latestStrapiReleaseTag;
return (
<Wrapper>
<Link to="/settings/application-infos">
<Icon />
<Text lineHeight="34px">
<FormattedMessage id="Settings.application.title" />
</Text>
{showNotif && <Notif />}
</Link>
</Wrapper>
);
};
export default memo(ApplicationDetailLink);

View File

@ -0,0 +1,8 @@
import styled from 'styled-components';
// background-color: red;
const MenuWrapper = styled.div`
background-color: ${props => props.theme.main.colors.mediumGrey};
`;
export default MenuWrapper;

View File

@ -2,7 +2,7 @@ import React, { memo } from 'react';
import { useGlobalContext } from 'strapi-helper-plugin'; import { useGlobalContext } from 'strapi-helper-plugin';
import { get } from 'lodash'; import { get } from 'lodash';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import PageTitle from '../../components/SettingsPageTitle'; import PageTitle from '../../../../components/SettingsPageTitle';
const SettingDispatcher = () => { const SettingDispatcher = () => {
const { plugins } = useGlobalContext(); const { plugins } = useGlobalContext();

View File

@ -0,0 +1,5 @@
export { default as ApplicationDetailLink } from './ApplicationDetailLink';
export { default as MenuWrapper } from './MenuWrapper';
export { default as SettingDispatcher } from './SettingDispatcher';
export { default as StyledLeftMenu } from './StyledLeftMenu';
export { default as Wrapper } from './Wrapper';

View File

@ -24,23 +24,28 @@ import HeaderSearch from '../../components/HeaderSearch';
import PageTitle from '../../components/PageTitle'; import PageTitle from '../../components/PageTitle';
import { useSettingsMenu } from '../../hooks'; import { useSettingsMenu } from '../../hooks';
import { retrieveGlobalLinks } from '../../utils'; import { retrieveGlobalLinks } from '../../utils';
import ApplicationInfosPage from '../ApplicationInfosPage';
import SettingsSearchHeaderProvider from '../SettingsHeaderSearchContextProvider'; import SettingsSearchHeaderProvider from '../SettingsHeaderSearchContextProvider';
import UsersEditPage from '../Users/ProtectedEditPage'; import UsersEditPage from '../Users/ProtectedEditPage';
import UsersListPage from '../Users/ProtectedListPage'; import UsersListPage from '../Users/ProtectedListPage';
import RolesEditPage from '../Roles/ProtectedEditPage'; import RolesEditPage from '../Roles/ProtectedEditPage';
import WebhooksCreateView from '../Webhooks/ProtectedCreateView';
import WebhooksEditView from '../Webhooks/ProtectedEditView';
import WebhooksListView from '../Webhooks/ProtectedListView';
import {
ApplicationDetailLink,
MenuWrapper,
SettingDispatcher,
StyledLeftMenu,
Wrapper,
} from './components';
import { import {
createRoute, createRoute,
findFirstAllowedEndpoint,
createPluginsLinksRoutes, createPluginsLinksRoutes,
makeUniqueRoutes, makeUniqueRoutes,
getSectionsToDisplay, getSectionsToDisplay,
} from './utils'; } from './utils';
import WebhooksCreateView from '../Webhooks/ProtectedCreateView';
import WebhooksEditView from '../Webhooks/ProtectedEditView';
import WebhooksListView from '../Webhooks/ProtectedListView';
import SettingDispatcher from './SettingDispatcher';
import LeftMenu from './StyledLeftMenu';
import Wrapper from './Wrapper';
function SettingsPage() { function SettingsPage() {
const { settingId } = useParams(); const { settingId } = useParams();
@ -50,14 +55,6 @@ function SettingsPage() {
const { isLoading, menu } = useSettingsMenu(); const { isLoading, menu } = useSettingsMenu();
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const pluginsGlobalLinks = useMemo(() => retrieveGlobalLinks(plugins), [plugins]); const pluginsGlobalLinks = useMemo(() => retrieveGlobalLinks(plugins), [plugins]);
const firstAvailableEndpoint = useMemo(() => {
// Don't need to compute while permissions are being checked
if (isLoading) {
return '';
}
return findFirstAllowedEndpoint(menu);
}, [menu, isLoading]);
// Create all the <Route /> that needs to be created by the plugins // Create all the <Route /> that needs to be created by the plugins
// For instance the upload plugin needs to create a <Route /> // For instance the upload plugin needs to create a <Route />
@ -95,8 +92,8 @@ function SettingsPage() {
return <LoadingIndicatorPage />; return <LoadingIndicatorPage />;
} }
if (!settingId && firstAvailableEndpoint) { if (!settingId) {
return <Redirect to={firstAvailableEndpoint} />; return <Redirect to={`${settingsBaseURL}/application-infos`} />;
} }
const settingTitle = formatMessage({ id: 'app.components.LeftMenuLinkContainer.settings' }); const settingTitle = formatMessage({ id: 'app.components.LeftMenuLinkContainer.settings' });
@ -109,14 +106,23 @@ function SettingsPage() {
<div className="row"> <div className="row">
<div className="col-md-3"> <div className="col-md-3">
<LeftMenu> <MenuWrapper>
{filteredMenu.map(item => { <ApplicationDetailLink />
return <LeftMenuList {...item} key={item.id} />; <StyledLeftMenu>
})} {filteredMenu.map(item => {
</LeftMenu> return <LeftMenuList {...item} key={item.id} />;
})}
</StyledLeftMenu>
</MenuWrapper>
</div> </div>
<div className="col-md-9"> <div className="col-md-9">
<Switch> <Switch>
<Route
exact
path={`${settingsBaseURL}/application-infos`}
component={ApplicationInfosPage}
/>
<Route exact path={`${settingsBaseURL}/roles`} component={ProtectedRolesListPage} /> <Route exact path={`${settingsBaseURL}/roles`} component={ProtectedRolesListPage} />
<Route <Route
exact exact

View File

@ -66,6 +66,13 @@
"Roles.RoleRow.user-count.plural": "{number} users", "Roles.RoleRow.user-count.plural": "{number} users",
"Roles.RoleRow.user-count.singular": "{number} user", "Roles.RoleRow.user-count.singular": "{number} user",
"Roles.components.List.empty.withSearch": "There is no role corresponding to the search ({search})...", "Roles.components.List.empty.withSearch": "There is no role corresponding to the search ({search})...",
"Settings.application.title": "Application",
"Settings.application.description": "See your project's details",
"Settings.application.link-pricing": "See all pricing",
"Settings.application.link-upgrade": "Upgrade your project",
"Settings.application.strapi-version": "STRAPI VERSION",
"Settings.application.node-version": "NODE VERSION",
"Settings.application.edition-title": "CURRENT PLAN",
"Settings.PageTitle": "Settings - {name}", "Settings.PageTitle": "Settings - {name}",
"Settings.error": "Error", "Settings.error": "Error",
"Settings.global": "Global Settings", "Settings.global": "Global Settings",