mirror of
https://github.com/strapi/strapi.git
synced 2025-08-07 00:09:23 +00:00
Merge pull request #8336 from strapi/front/application-settings
Add application details page
This commit is contained in:
commit
e26b45e6be
@ -1,6 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const A = styled.a`
|
||||
display: flex;
|
||||
position: relative;
|
||||
padding-top: 0.7rem;
|
||||
padding-bottom: 0.2rem;
|
||||
@ -10,7 +11,6 @@ const A = styled.a`
|
||||
cursor: pointer;
|
||||
color: ${props => props.theme.main.colors.leftMenu['link-color']};
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
&:hover {
|
||||
@ -31,6 +31,8 @@ const A = styled.a`
|
||||
}
|
||||
|
||||
&.linkActive {
|
||||
padding-right: 2.3rem;
|
||||
|
||||
color: white !important;
|
||||
border-left: 0.3rem solid ${props => props.theme.main.colors.strapi.blue};
|
||||
}
|
||||
|
@ -10,10 +10,10 @@ import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import styled from 'styled-components';
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
|
||||
import en from '../../../translations/en.json';
|
||||
import LeftMenuIcon from './LeftMenuIcon';
|
||||
import A from './A';
|
||||
import NotificationCount from './NotificationCount';
|
||||
|
||||
const LinkLabel = styled.span`
|
||||
display: inline-block;
|
||||
@ -23,7 +23,7 @@ const LinkLabel = styled.span`
|
||||
`;
|
||||
|
||||
// TODO: refacto this file
|
||||
const LeftMenuLinkContent = ({ destination, iconName, label, location }) => {
|
||||
const LeftMenuLinkContent = ({ destination, iconName, label, location, notificationsCount }) => {
|
||||
const isLinkActive = startsWith(
|
||||
location.pathname.replace('/admin', '').concat('/'),
|
||||
destination.concat('/')
|
||||
@ -67,6 +67,7 @@ const LeftMenuLinkContent = ({ destination, iconName, label, location }) => {
|
||||
>
|
||||
<LeftMenuIcon icon={iconName} />
|
||||
{content}
|
||||
{notificationsCount > 0 && <NotificationCount count={notificationsCount} />}
|
||||
</A>
|
||||
);
|
||||
};
|
||||
@ -78,6 +79,7 @@ LeftMenuLinkContent.propTypes = {
|
||||
location: PropTypes.shape({
|
||||
pathname: PropTypes.string,
|
||||
}).isRequired,
|
||||
notificationsCount: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export default withRouter(LeftMenuLinkContent);
|
||||
|
@ -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;
|
@ -9,13 +9,14 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import LeftMenuLinkContent from './LeftMenuLinkContent';
|
||||
|
||||
const LeftMenuLink = ({ destination, iconName, label, location }) => {
|
||||
const LeftMenuLink = ({ destination, iconName, label, location, notificationsCount }) => {
|
||||
return (
|
||||
<LeftMenuLinkContent
|
||||
destination={destination}
|
||||
iconName={iconName}
|
||||
label={label}
|
||||
location={location}
|
||||
notificationsCount={notificationsCount}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -27,6 +28,7 @@ LeftMenuLink.propTypes = {
|
||||
location: PropTypes.shape({
|
||||
pathname: PropTypes.string,
|
||||
}).isRequired,
|
||||
notificationsCount: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
LeftMenuLink.defaultProps = {
|
||||
|
@ -3,7 +3,6 @@ import styled from 'styled-components';
|
||||
const LeftMenuListLink = styled.div`
|
||||
max-height: 180px;
|
||||
margin-bottom: 19px;
|
||||
margin-right: 28px;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
|
@ -46,6 +46,7 @@ const LeftMenuLinksSection = ({
|
||||
iconName={link.icon}
|
||||
label={link.label}
|
||||
destination={link.destination}
|
||||
notificationsCount={link.notificationsCount || 0}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
|
@ -5,12 +5,20 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
GET_STRAPI_LATEST_RELEASE_SUCCEEDED,
|
||||
GET_USER_PERMISSIONS,
|
||||
GET_USER_PERMISSIONS_ERROR,
|
||||
GET_USER_PERMISSIONS_SUCCEEDED,
|
||||
SET_APP_ERROR,
|
||||
} from './constants';
|
||||
|
||||
export function getStrapiLatestReleaseSucceeded(latestStrapiReleaseTag) {
|
||||
return {
|
||||
type: GET_STRAPI_LATEST_RELEASE_SUCCEEDED,
|
||||
latestStrapiReleaseTag,
|
||||
};
|
||||
}
|
||||
|
||||
export function getUserPermissions() {
|
||||
return {
|
||||
type: GET_USER_PERMISSIONS,
|
||||
|
@ -5,6 +5,8 @@
|
||||
*/
|
||||
|
||||
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_ERROR = 'StrapiAdmin/Admin/GET_USER_PERMISSIONS_ERROR';
|
||||
export const GET_USER_PERMISSIONS_SUCCEEDED = 'StrapiAdmin/Admin/GET_USER_PERMISSIONS_SUCCEEDED';
|
||||
|
@ -23,7 +23,7 @@ import {
|
||||
CheckPagePermissions,
|
||||
request,
|
||||
} 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 Header from '../../components/Header/index';
|
||||
@ -42,10 +42,12 @@ import Logout from './Logout';
|
||||
import {
|
||||
disableGlobalOverlayBlocker,
|
||||
enableGlobalOverlayBlocker,
|
||||
getInfosDataSucceeded,
|
||||
updatePlugin,
|
||||
} from '../App/actions';
|
||||
import makeSelecApp from '../App/selectors';
|
||||
import {
|
||||
getStrapiLatestReleaseSucceeded,
|
||||
getUserPermissions,
|
||||
getUserPermissionsError,
|
||||
getUserPermissionsSucceeded,
|
||||
@ -67,7 +69,7 @@ export class Admin extends React.Component {
|
||||
|
||||
componentDidMount() {
|
||||
this.emitEvent('didAccessAuthenticatedAdministration');
|
||||
this.fetchUserPermissions(true);
|
||||
this.initApp();
|
||||
}
|
||||
|
||||
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) => {
|
||||
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);
|
||||
};
|
||||
|
||||
initApp = async () => {
|
||||
await this.fetchAppInfo();
|
||||
await this.fetchStrapiLatestRelease();
|
||||
await this.fetchUserPermissions(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Display the app loader until the app is ready
|
||||
* @returns {Boolean}
|
||||
@ -170,7 +231,7 @@ export class Admin extends React.Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
admin: { isLoading, userPermissions },
|
||||
admin: { isLoading, latestStrapiReleaseTag, userPermissions },
|
||||
global: {
|
||||
autoReload,
|
||||
blockApp,
|
||||
@ -211,14 +272,21 @@ export class Admin extends React.Component {
|
||||
enableGlobalOverlayBlocker={enableGlobalOverlayBlocker}
|
||||
fetchUserPermissions={this.fetchUserPermissions}
|
||||
formatMessage={formatMessage}
|
||||
latestStrapiReleaseTag={latestStrapiReleaseTag}
|
||||
menu={this.menuRef.current}
|
||||
plugins={plugins}
|
||||
settingsBaseURL={SETTINGS_BASE_URL || '/settings'}
|
||||
strapiVersion={strapiVersion}
|
||||
updatePlugin={updatePlugin}
|
||||
>
|
||||
<UserProvider value={userPermissions}>
|
||||
<Wrapper>
|
||||
<LeftMenu version={strapiVersion} plugins={plugins} ref={this.menuRef} />
|
||||
<LeftMenu
|
||||
latestStrapiReleaseTag={latestStrapiReleaseTag}
|
||||
version={strapiVersion}
|
||||
plugins={plugins}
|
||||
ref={this.menuRef}
|
||||
/>
|
||||
<NavTopRightWrapper>
|
||||
{/* Injection zone not ready yet */}
|
||||
<Logout />
|
||||
@ -275,10 +343,13 @@ Admin.propTypes = {
|
||||
admin: PropTypes.shape({
|
||||
appError: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
latestStrapiReleaseTag: PropTypes.string.isRequired,
|
||||
userPermissions: PropTypes.array,
|
||||
}).isRequired,
|
||||
disableGlobalOverlayBlocker: PropTypes.func.isRequired,
|
||||
enableGlobalOverlayBlocker: PropTypes.func.isRequired,
|
||||
getInfosDataSucceeded: PropTypes.func.isRequired,
|
||||
getStrapiLatestReleaseSucceeded: PropTypes.func.isRequired,
|
||||
getUserPermissions: PropTypes.func.isRequired,
|
||||
getUserPermissionsError: PropTypes.func.isRequired,
|
||||
getUserPermissionsSucceeded: PropTypes.func.isRequired,
|
||||
@ -311,6 +382,8 @@ export function mapDispatchToProps(dispatch) {
|
||||
{
|
||||
disableGlobalOverlayBlocker,
|
||||
enableGlobalOverlayBlocker,
|
||||
getInfosDataSucceeded,
|
||||
getStrapiLatestReleaseSucceeded,
|
||||
getUserPermissions,
|
||||
getUserPermissionsError,
|
||||
getUserPermissionsSucceeded,
|
||||
|
@ -5,17 +5,21 @@
|
||||
*/
|
||||
|
||||
import produce from 'immer';
|
||||
import packageJSON from '../../../../package.json';
|
||||
|
||||
import {
|
||||
GET_STRAPI_LATEST_RELEASE_SUCCEEDED,
|
||||
GET_USER_PERMISSIONS,
|
||||
GET_USER_PERMISSIONS_ERROR,
|
||||
GET_USER_PERMISSIONS_SUCCEEDED,
|
||||
SET_APP_ERROR,
|
||||
} from './constants';
|
||||
|
||||
const packageVersion = packageJSON.version;
|
||||
const initialState = {
|
||||
appError: false,
|
||||
isLoading: true,
|
||||
latestStrapiReleaseTag: `v${packageVersion}`,
|
||||
userPermissions: [],
|
||||
};
|
||||
|
||||
@ -23,6 +27,10 @@ const reducer = (state = initialState, action) =>
|
||||
// eslint-disable-next-line consistent-return
|
||||
produce(state, draftState => {
|
||||
switch (action.type) {
|
||||
case GET_STRAPI_LATEST_RELEASE_SUCCEEDED: {
|
||||
draftState.latestStrapiReleaseTag = action.latestStrapiReleaseTag;
|
||||
break;
|
||||
}
|
||||
case GET_USER_PERMISSIONS: {
|
||||
draftState.isLoading = true;
|
||||
break;
|
||||
|
@ -19,10 +19,13 @@ describe('<Admin />', () => {
|
||||
props = {
|
||||
admin: {
|
||||
appError: false,
|
||||
latestStrapiReleaseTag: '3',
|
||||
},
|
||||
disableGlobalOverlayBlocker: jest.fn(),
|
||||
emitEvent: jest.fn(),
|
||||
enableGlobalOverlayBlocker: jest.fn(),
|
||||
getInfosDataSucceeded: jest.fn(),
|
||||
getStrapiLatestReleaseSucceeded: jest.fn(),
|
||||
getUserPermissions: jest.fn(),
|
||||
getUserPermissionsError: jest.fn(),
|
||||
getUserPermissionsSucceeded: jest.fn(),
|
||||
@ -42,6 +45,7 @@ describe('<Admin />', () => {
|
||||
intl: {
|
||||
formatMessage: jest.fn(),
|
||||
},
|
||||
|
||||
location: {},
|
||||
setAppError: jest.fn(),
|
||||
showGlobalAppBlocker: jest.fn(),
|
||||
|
@ -1,4 +1,5 @@
|
||||
import produce from 'immer';
|
||||
import packageJSON from '../../../../../package.json';
|
||||
import {
|
||||
setAppError,
|
||||
getUserPermissions,
|
||||
@ -14,6 +15,7 @@ describe('adminReducer', () => {
|
||||
state = {
|
||||
appError: false,
|
||||
isLoading: true,
|
||||
latestStrapiReleaseTag: `v${packageJSON.version}`,
|
||||
userPermissions: [],
|
||||
};
|
||||
});
|
||||
|
@ -8,12 +8,12 @@ import {
|
||||
DISABLE_GLOBAL_OVERLAY_BLOCKER,
|
||||
ENABLE_GLOBAL_OVERLAY_BLOCKER,
|
||||
FREEZE_APP,
|
||||
GET_INFOS_DATA_SUCCEEDED,
|
||||
GET_DATA_SUCCEEDED,
|
||||
LOAD_PLUGIN,
|
||||
PLUGIN_DELETED,
|
||||
PLUGIN_LOADED,
|
||||
UNFREEZE_APP,
|
||||
UNSET_HAS_USERS_PLUGIN,
|
||||
UPDATE_PLUGIN,
|
||||
} 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) {
|
||||
return {
|
||||
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) {
|
||||
return {
|
||||
type: UPDATE_PLUGIN,
|
||||
|
@ -9,9 +9,10 @@ export const LOAD_PLUGIN = 'app/App/LOAD_PLUGIN';
|
||||
export const PLUGIN_LOADED = 'app/App/PLUGIN_LOADED';
|
||||
export const PLUGIN_DELETED = 'app/App/PLUGIN_DELETED';
|
||||
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 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 GET_DATA_SUCCEEDED = 'app/App/GET_DATA_SUCCEEDED';
|
||||
export const GET_INFOS_DATA_SUCCEEDED = 'admin/App/GET_INFOS_DATA_SUCCEEDED';
|
||||
|
@ -35,25 +35,31 @@ function App(props) {
|
||||
getDataRef.current = props.getDataSucceeded;
|
||||
|
||||
useEffect(() => {
|
||||
const getData = async () => {
|
||||
const currentToken = auth.getToken();
|
||||
const currentToken = auth.getToken();
|
||||
|
||||
if (currentToken) {
|
||||
try {
|
||||
const {
|
||||
data: { token },
|
||||
} = await request('/admin/renew-token', {
|
||||
method: 'POST',
|
||||
body: { token: currentToken },
|
||||
});
|
||||
auth.updateToken(token);
|
||||
} catch (err) {
|
||||
// Refresh app
|
||||
auth.clearAppStorage();
|
||||
window.location.reload();
|
||||
}
|
||||
const renewToken = async () => {
|
||||
try {
|
||||
const {
|
||||
data: { token },
|
||||
} = await request('/admin/renew-token', {
|
||||
method: 'POST',
|
||||
body: { token: currentToken },
|
||||
});
|
||||
auth.updateToken(token);
|
||||
} catch (err) {
|
||||
// Refresh app
|
||||
auth.clearAppStorage();
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
if (currentToken) {
|
||||
renewToken();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const getData = async () => {
|
||||
try {
|
||||
const { data } = await request('/admin/init', { method: 'GET' });
|
||||
|
||||
@ -87,7 +93,7 @@ function App(props) {
|
||||
};
|
||||
|
||||
getData();
|
||||
}, [getDataRef]);
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingIndicatorPage />;
|
||||
|
@ -6,16 +6,17 @@ import {
|
||||
DISABLE_GLOBAL_OVERLAY_BLOCKER,
|
||||
ENABLE_GLOBAL_OVERLAY_BLOCKER,
|
||||
FREEZE_APP,
|
||||
GET_INFOS_DATA_SUCCEEDED,
|
||||
GET_DATA_SUCCEEDED,
|
||||
PLUGIN_DELETED,
|
||||
PLUGIN_LOADED,
|
||||
UNFREEZE_APP,
|
||||
UNSET_HAS_USERS_PLUGIN,
|
||||
UPDATE_PLUGIN,
|
||||
} from './constants';
|
||||
|
||||
const packageVersion = packageJSON.version;
|
||||
const initialState = fromJS({
|
||||
appInfos: {},
|
||||
autoReload: false,
|
||||
blockApp: false,
|
||||
currentEnvironment: 'development',
|
||||
@ -43,24 +44,27 @@ function appReducer(state = initialState, action) {
|
||||
|
||||
return null;
|
||||
});
|
||||
case GET_DATA_SUCCEEDED: {
|
||||
const {
|
||||
data: { hasAdmin, uuid, currentEnvironment, autoReload, strapiVersion },
|
||||
} = action;
|
||||
|
||||
if (strapiVersion !== state.get('strapiVersion')) {
|
||||
case GET_INFOS_DATA_SUCCEEDED: {
|
||||
if (action.data.strapiVersion !== state.get('strapiVersion')) {
|
||||
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');
|
||||
}
|
||||
|
||||
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
|
||||
.update('isLoading', () => false)
|
||||
.update('hasAdminUser', () => hasAdmin)
|
||||
.update('uuid', () => uuid)
|
||||
.update('autoReload', () => autoReload)
|
||||
.update('currentEnvironment', () => currentEnvironment);
|
||||
.update('hasAdminUser', () => action.data.hasAdmin)
|
||||
.update('uuid', () => action.data.uuid);
|
||||
}
|
||||
case PLUGIN_LOADED:
|
||||
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]);
|
||||
case UNFREEZE_APP:
|
||||
return state.set('blockApp', false).set('overlayBlockerData', null);
|
||||
case UNSET_HAS_USERS_PLUGIN:
|
||||
return state.set('hasUserPlugin', false);
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
@ -9,47 +9,22 @@ const selectApp = () => state => state.get('app');
|
||||
* Select the language locale
|
||||
*/
|
||||
|
||||
const selectPlugins = () =>
|
||||
createSelector(
|
||||
selectApp(),
|
||||
appState => appState.get('plugins')
|
||||
);
|
||||
const selectPlugins = () => createSelector(selectApp(), appState => appState.get('plugins'));
|
||||
|
||||
const makeSelectApp = () =>
|
||||
createSelector(
|
||||
selectApp(),
|
||||
appState => appState.toJS()
|
||||
);
|
||||
const makeSelectApp = () => createSelector(selectApp(), appState => appState.toJS());
|
||||
|
||||
const selectHasUserPlugin = () =>
|
||||
createSelector(
|
||||
selectApp(),
|
||||
appState => appState.get('hasUserPlugin')
|
||||
);
|
||||
createSelector(selectApp(), appState => appState.get('hasUserPlugin'));
|
||||
|
||||
const makeSelectShowGlobalAppBlocker = () =>
|
||||
createSelector(
|
||||
selectApp(),
|
||||
appState => appState.get('showGlobalAppBlocker')
|
||||
);
|
||||
createSelector(selectApp(), appState => appState.get('showGlobalAppBlocker'));
|
||||
|
||||
const makeSelectBlockApp = () =>
|
||||
createSelector(
|
||||
selectApp(),
|
||||
appState => appState.get('blockApp')
|
||||
);
|
||||
const makeSelectBlockApp = () => createSelector(selectApp(), appState => appState.get('blockApp'));
|
||||
|
||||
const makeSelectOverlayBlockerProps = () =>
|
||||
createSelector(
|
||||
selectApp(),
|
||||
appState => appState.get('overlayBlockerData')
|
||||
);
|
||||
createSelector(selectApp(), appState => appState.get('overlayBlockerData'));
|
||||
|
||||
const makeSelectUuid = () =>
|
||||
createSelector(
|
||||
selectApp(),
|
||||
appState => appState.get('uuid')
|
||||
);
|
||||
const makeSelectUuid = () => createSelector(selectApp(), appState => appState.get('uuid'));
|
||||
|
||||
export default makeSelectApp;
|
||||
export {
|
||||
|
@ -1,19 +1,21 @@
|
||||
import {
|
||||
FREEZE_APP,
|
||||
GET_DATA_SUCCEEDED,
|
||||
GET_INFOS_DATA_SUCCEEDED,
|
||||
LOAD_PLUGIN,
|
||||
PLUGIN_DELETED,
|
||||
PLUGIN_LOADED,
|
||||
UNFREEZE_APP,
|
||||
UNSET_HAS_USERS_PLUGIN,
|
||||
UPDATE_PLUGIN,
|
||||
} from '../constants';
|
||||
import {
|
||||
freezeApp,
|
||||
loadPlugin,
|
||||
getInfosDataSucceeded,
|
||||
getDataSucceeded,
|
||||
pluginDeleted,
|
||||
pluginLoaded,
|
||||
unfreezeApp,
|
||||
unsetHasUserPlugin,
|
||||
updatePlugin,
|
||||
} 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', () => {
|
||||
it('should return the correct type and the passed data', () => {
|
||||
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', () => {
|
||||
it('should return the correct type and the passed data', () => {
|
||||
const pluginId = 'content-manager';
|
||||
@ -104,9 +120,7 @@ describe('<App /> actions', () => {
|
||||
updatedValue,
|
||||
};
|
||||
|
||||
expect(updatePlugin(pluginId, updatedKey, updatedValue)).toEqual(
|
||||
expected
|
||||
);
|
||||
expect(updatePlugin(pluginId, updatedKey, updatedValue)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -4,10 +4,11 @@ import {
|
||||
disableGlobalOverlayBlocker,
|
||||
enableGlobalOverlayBlocker,
|
||||
freezeApp,
|
||||
getDataSucceeded,
|
||||
getInfosDataSucceeded,
|
||||
pluginDeleted,
|
||||
pluginLoaded,
|
||||
unfreezeApp,
|
||||
unsetHasUserPlugin,
|
||||
updatePlugin,
|
||||
} from '../actions';
|
||||
import appReducer from '../reducer';
|
||||
@ -17,6 +18,7 @@ describe('<App /> reducer', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
state = fromJS({
|
||||
appInfos: {},
|
||||
autoReload: false,
|
||||
blockApp: false,
|
||||
currentEnvironment: 'development',
|
||||
@ -96,9 +98,34 @@ describe('<App /> reducer', () => {
|
||||
expect(appReducer(state, unfreezeApp())).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should handle the unsetHasUserPlugin action correclty', () => {
|
||||
const expectedResult = state.set('hasUserPlugin', false);
|
||||
describe('GET_INFOS_DATA_SUCCEEDED', () => {
|
||||
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
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -0,0 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
width: 50%;
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
@ -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';
|
@ -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);
|
@ -4,16 +4,12 @@
|
||||
*
|
||||
*/
|
||||
/* eslint-disable */
|
||||
import React, { memo, useMemo, useEffect } from 'react';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { get, isNil, upperFirst } from 'lodash';
|
||||
import { get, upperFirst } from 'lodash';
|
||||
import { auth, LoadingIndicatorPage } from 'strapi-helper-plugin';
|
||||
import axios from 'axios';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import PageTitle from '../../components/PageTitle';
|
||||
import { useModels } from '../../hooks';
|
||||
import { STRAPI_UPDATE_NOTIF } from '../../config';
|
||||
|
||||
import useFetch from './hooks';
|
||||
import { ALink, Block, Container, LinkWrapper, P, Wave, Separator } from './components';
|
||||
@ -65,54 +61,7 @@ const SOCIAL_LINKS = [
|
||||
},
|
||||
];
|
||||
|
||||
const HomePage = ({ global: { strapiVersion }, 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 HomePage = ({ history: { push } }) => {
|
||||
const { error, isLoading, posts } = useFetch();
|
||||
// Temporary until we develop the menu API
|
||||
const { collectionTypes, singleTypes, isLoading: isLoadingForModels } = useModels();
|
||||
|
@ -30,10 +30,19 @@ import reducer, { initialState } from './reducer';
|
||||
import Loader from './Loader';
|
||||
import Wrapper from './Wrapper';
|
||||
|
||||
const LeftMenu = forwardRef(({ version, plugins }, ref) => {
|
||||
const LeftMenu = forwardRef(({ latestStrapiReleaseTag, version, plugins }, ref) => {
|
||||
const location = useLocation();
|
||||
const permissions = useContext(UserContext);
|
||||
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 [
|
||||
{
|
||||
collectionTypesSectionLinks,
|
||||
@ -43,7 +52,9 @@ const LeftMenu = forwardRef(({ version, plugins }, ref) => {
|
||||
singleTypesSectionLinks,
|
||||
},
|
||||
dispatch,
|
||||
] = useReducer(reducer, initialState, () => init(initialState, plugins, settingsMenu));
|
||||
] = useReducer(reducer, initialState, () =>
|
||||
init(initialState, plugins, settingsMenu, settingsLinkNotificationCount)
|
||||
);
|
||||
const generalSectionLinksFiltered = useMemo(() => filterLinks(generalSectionLinks), [
|
||||
generalSectionLinks,
|
||||
]);
|
||||
@ -197,6 +208,7 @@ const LeftMenu = forwardRef(({ version, plugins }, ref) => {
|
||||
});
|
||||
|
||||
LeftMenu.propTypes = {
|
||||
latestStrapiReleaseTag: PropTypes.string.isRequired,
|
||||
version: PropTypes.string.isRequired,
|
||||
plugins: PropTypes.object.isRequired,
|
||||
};
|
||||
|
@ -3,7 +3,7 @@ import { SETTINGS_BASE_URL } from '../../config';
|
||||
import { sortLinks } from '../../utils';
|
||||
import { getSettingsMenuLinksPermissions } from './utils';
|
||||
|
||||
const init = (initialState, plugins = {}, settingsMenu = []) => {
|
||||
const init = (initialState, plugins = {}, settingsMenu = [], settingsLinkNotificationCount = 0) => {
|
||||
const settingsLinkPermissions = getSettingsMenuLinksPermissions(settingsMenu);
|
||||
|
||||
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) {
|
||||
const permissionsPath = ['generalSectionLinks', settingsLinkIndex, 'permissions'];
|
||||
const notificationPath = ['generalSectionLinks', settingsLinkIndex, 'notificationsCount'];
|
||||
|
||||
set(initialState, permissionsPath, settingsLinkPermissions);
|
||||
set(initialState, notificationPath, settingsLinkNotificationCount);
|
||||
}
|
||||
|
||||
if (sortedLinks.length) {
|
||||
|
@ -13,6 +13,7 @@ const initialState = {
|
||||
destination: '/list-plugins',
|
||||
isDisplayed: false,
|
||||
permissions: adminPermissions.marketplace.main,
|
||||
notificationsCount: 0,
|
||||
},
|
||||
{
|
||||
icon: 'shopping-basket',
|
||||
@ -20,6 +21,7 @@ const initialState = {
|
||||
destination: '/marketplace',
|
||||
isDisplayed: false,
|
||||
permissions: adminPermissions.marketplace.main,
|
||||
notificationsCount: 0,
|
||||
},
|
||||
{
|
||||
icon: 'cog',
|
||||
@ -29,6 +31,7 @@ const initialState = {
|
||||
// Permissions of this link are retrieved in the init phase
|
||||
// using the settings menu
|
||||
permissions: [],
|
||||
notificationsCount: 0,
|
||||
},
|
||||
],
|
||||
singleTypesSectionLinks: [],
|
||||
|
@ -112,6 +112,7 @@ describe('ADMIN | LeftMenu | init', () => {
|
||||
title: 'Settings.webhooks.title',
|
||||
to: '/settings/webhooks',
|
||||
name: 'webhooks',
|
||||
|
||||
permissions: [
|
||||
{ action: 'admin::webhook.create', subject: null },
|
||||
{ action: 'admin::webhook.read', subject: null },
|
||||
@ -223,6 +224,7 @@ describe('ADMIN | LeftMenu | init', () => {
|
||||
label: 'app.components.LeftMenuLinkContainer.settings',
|
||||
isDisplayed: false,
|
||||
destination: SETTINGS_BASE_URL,
|
||||
notificationsCount: 0,
|
||||
permissions: [
|
||||
// webhooks
|
||||
{ action: 'admin::webhook.create', subject: null },
|
||||
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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);
|
@ -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;
|
@ -2,7 +2,7 @@ import React, { memo } from 'react';
|
||||
import { useGlobalContext } from 'strapi-helper-plugin';
|
||||
import { get } from 'lodash';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import PageTitle from '../../components/SettingsPageTitle';
|
||||
import PageTitle from '../../../../components/SettingsPageTitle';
|
||||
|
||||
const SettingDispatcher = () => {
|
||||
const { plugins } = useGlobalContext();
|
@ -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';
|
@ -24,23 +24,28 @@ import HeaderSearch from '../../components/HeaderSearch';
|
||||
import PageTitle from '../../components/PageTitle';
|
||||
import { useSettingsMenu } from '../../hooks';
|
||||
import { retrieveGlobalLinks } from '../../utils';
|
||||
import ApplicationInfosPage from '../ApplicationInfosPage';
|
||||
import SettingsSearchHeaderProvider from '../SettingsHeaderSearchContextProvider';
|
||||
import UsersEditPage from '../Users/ProtectedEditPage';
|
||||
import UsersListPage from '../Users/ProtectedListPage';
|
||||
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 {
|
||||
createRoute,
|
||||
findFirstAllowedEndpoint,
|
||||
createPluginsLinksRoutes,
|
||||
makeUniqueRoutes,
|
||||
getSectionsToDisplay,
|
||||
} 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() {
|
||||
const { settingId } = useParams();
|
||||
@ -50,14 +55,6 @@ function SettingsPage() {
|
||||
const { isLoading, menu } = useSettingsMenu();
|
||||
const { formatMessage } = useIntl();
|
||||
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
|
||||
// For instance the upload plugin needs to create a <Route />
|
||||
@ -95,8 +92,8 @@ function SettingsPage() {
|
||||
return <LoadingIndicatorPage />;
|
||||
}
|
||||
|
||||
if (!settingId && firstAvailableEndpoint) {
|
||||
return <Redirect to={firstAvailableEndpoint} />;
|
||||
if (!settingId) {
|
||||
return <Redirect to={`${settingsBaseURL}/application-infos`} />;
|
||||
}
|
||||
|
||||
const settingTitle = formatMessage({ id: 'app.components.LeftMenuLinkContainer.settings' });
|
||||
@ -109,14 +106,23 @@ function SettingsPage() {
|
||||
|
||||
<div className="row">
|
||||
<div className="col-md-3">
|
||||
<LeftMenu>
|
||||
{filteredMenu.map(item => {
|
||||
return <LeftMenuList {...item} key={item.id} />;
|
||||
})}
|
||||
</LeftMenu>
|
||||
<MenuWrapper>
|
||||
<ApplicationDetailLink />
|
||||
<StyledLeftMenu>
|
||||
{filteredMenu.map(item => {
|
||||
return <LeftMenuList {...item} key={item.id} />;
|
||||
})}
|
||||
</StyledLeftMenu>
|
||||
</MenuWrapper>
|
||||
</div>
|
||||
|
||||
<div className="col-md-9">
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path={`${settingsBaseURL}/application-infos`}
|
||||
component={ApplicationInfosPage}
|
||||
/>
|
||||
<Route exact path={`${settingsBaseURL}/roles`} component={ProtectedRolesListPage} />
|
||||
<Route
|
||||
exact
|
||||
|
@ -66,6 +66,13 @@
|
||||
"Roles.RoleRow.user-count.plural": "{number} users",
|
||||
"Roles.RoleRow.user-count.singular": "{number} user",
|
||||
"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.error": "Error",
|
||||
"Settings.global": "Global Settings",
|
||||
|
Loading…
x
Reference in New Issue
Block a user