Change adminReducer to immer

Signed-off-by: soupette <cyril.lpz@gmail.com>
This commit is contained in:
soupette 2020-06-16 11:31:46 +02:00 committed by Alexandre Bodin
parent 8b02624b6b
commit b64af2c309
15 changed files with 332 additions and 178 deletions

View File

@ -4,12 +4,30 @@
*
*/
import { GET_PLUGINS_FROM_MARKETPLACE_SUCCEEDED, SET_APP_ERROR } from './constants';
import {
GET_USER_PERMISSIONS,
GET_USER_PERMISSIONS_ERROR,
GET_USER_PERMISSIONS_SUCCEEDED,
SET_APP_ERROR,
} from './constants';
export function getPluginsFromMarketPlaceSucceeded(plugins) {
export function getUserPermissions() {
return {
type: GET_PLUGINS_FROM_MARKETPLACE_SUCCEEDED,
plugins,
type: GET_USER_PERMISSIONS,
};
}
export function getUserPermissionsError(error) {
return {
type: GET_USER_PERMISSIONS_ERROR,
error,
};
}
export function getUserPermissionsSucceeded(data) {
return {
type: GET_USER_PERMISSIONS_SUCCEEDED,
data,
};
}

View File

@ -4,6 +4,7 @@
*
*/
export const GET_PLUGINS_FROM_MARKETPLACE_SUCCEEDED =
'StrapiAdmin/Admin/GET_PLUGINS_FROM_MARKETPLACE_SUCCEEDED';
export const SET_APP_ERROR = 'StrapiAdmin/Admin/SET_APP_ERROR';
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';

View File

@ -45,7 +45,12 @@ import {
updatePlugin,
} from '../App/actions';
import makeSelecApp from '../App/selectors';
import { setAppError } from './actions';
import {
getUserPermissions,
getUserPermissionsError,
getUserPermissionsSucceeded,
setAppError,
} from './actions';
import makeSelectAdmin from './selectors';
import Wrapper from './Wrapper';
import Content from './Content';
@ -62,6 +67,9 @@ export class Admin extends React.Component {
componentDidMount() {
this.emitEvent('didAccessAuthenticatedAdministration');
// TODO: remove the user1 it is just for testing until
// Api is ready
this.fetchUserPermissions('user1', true);
}
shouldComponentUpdate(prevProps) {
@ -100,6 +108,28 @@ export class Admin extends React.Component {
}
};
fetchUserPermissions = async (user = 'user1', resetState = false) => {
const { getUserPermissions, getUserPermissionsError, getUserPermissionsSucceeded } = this.props;
if (resetState) {
// Show a loader
getUserPermissions();
}
try {
const data = await new Promise(resolve =>
setTimeout(() => {
resolve(fakePermissionsData[user]);
}, 2000)
);
getUserPermissionsSucceeded(data);
} catch (err) {
console.error(err);
getUserPermissionsError(err);
}
};
hasApluginNotReady = props => {
const {
global: { plugins },
@ -144,6 +174,7 @@ export class Admin extends React.Component {
render() {
const {
admin: { isLoading, userPermissions },
global: {
autoReload,
blockApp,
@ -169,6 +200,13 @@ export class Admin extends React.Component {
);
}
// Show a loader while permissions are being fetched
// TODO: we might need to improve this behavior for the ctb
// since we will need to update the permissions
if (isLoading) {
return <LoadingIndicatorPage />;
}
return (
<GlobalContextProvider
autoReload={autoReload}
@ -177,13 +215,14 @@ export class Admin extends React.Component {
currentLocale={locale}
disableGlobalOverlayBlocker={disableGlobalOverlayBlocker}
enableGlobalOverlayBlocker={enableGlobalOverlayBlocker}
fetchUserPermissions={this.fetchUserPermissions}
formatMessage={formatMessage}
menu={this.menuRef.current}
plugins={plugins}
settingsBaseURL={SETTINGS_BASE_URL || '/settings'}
menu={this.menuRef.current}
updatePlugin={updatePlugin}
>
<UserProvider value={fakePermissionsData.user2}>
<UserProvider value={userPermissions}>
<Wrapper>
<LeftMenu version={strapiVersion} plugins={plugins} ref={this.menuRef} />
<NavTopRightWrapper>
@ -243,9 +282,14 @@ Admin.defaultProps = {
Admin.propTypes = {
admin: PropTypes.shape({
appError: PropTypes.bool,
isLoading: PropTypes.bool,
userPermissions: PropTypes.array,
}).isRequired,
disableGlobalOverlayBlocker: PropTypes.func.isRequired,
enableGlobalOverlayBlocker: PropTypes.func.isRequired,
getUserPermissions: PropTypes.func.isRequired,
getUserPermissionsError: PropTypes.func.isRequired,
getUserPermissionsSucceeded: PropTypes.func.isRequired,
global: PropTypes.shape({
autoReload: PropTypes.bool,
blockApp: PropTypes.bool,
@ -275,6 +319,9 @@ export function mapDispatchToProps(dispatch) {
{
disableGlobalOverlayBlocker,
enableGlobalOverlayBlocker,
getUserPermissions,
getUserPermissionsError,
getUserPermissionsSucceeded,
setAppError,
updatePlugin,
},

View File

@ -4,23 +4,48 @@
*
*/
import { fromJS } from 'immutable';
import { GET_PLUGINS_FROM_MARKETPLACE_SUCCEEDED, SET_APP_ERROR } from './constants';
import produce from 'immer';
const initialState = fromJS({
import {
GET_USER_PERMISSIONS,
GET_USER_PERMISSIONS_ERROR,
GET_USER_PERMISSIONS_SUCCEEDED,
SET_APP_ERROR,
} from './constants';
const initialState = {
appError: false,
pluginsFromMarketplace: [],
});
isLoading: true,
userPermissions: [],
};
function adminReducer(state = initialState, action) {
switch (action.type) {
case GET_PLUGINS_FROM_MARKETPLACE_SUCCEEDED:
return state.update('pluginsFromMarketplace', () => fromJS(action.plugins));
case SET_APP_ERROR:
return state.update('appError', () => true);
default:
return state;
}
}
const reducer = (state = initialState, action) =>
// eslint-disable-next-line consistent-return
produce(state, draftState => {
switch (action.type) {
case GET_USER_PERMISSIONS: {
draftState.isLoading = true;
break;
}
export default adminReducer;
case GET_USER_PERMISSIONS_ERROR: {
draftState.error = action.error;
draftState.isLoading = false;
break;
}
case GET_USER_PERMISSIONS_SUCCEEDED: {
draftState.isLoading = false;
draftState.userPermissions = action.data;
break;
}
case SET_APP_ERROR: {
draftState.appError = true;
break;
}
default:
return state;
}
});
export default reducer;
export { initialState };

View File

@ -1,9 +1,12 @@
import { createSelector } from 'reselect';
import { initialState } from './reducer';
/**
* Direct selector to the admin state domain
*/
const selectAdminDomain = () => state => state.get('admin');
const selectAdminDomain = () => state => {
return state.get('admin') || initialState;
};
/**
* Other specific selectors
@ -13,12 +16,7 @@ const selectAdminDomain = () => state => state.get('admin');
* Default selector used by Admin
*/
const makeSelectAdmin = () => createSelector(selectAdminDomain(), substate => substate.toJS());
const makeSelectPluginsFromMarketplace = () =>
createSelector(selectAdminDomain(), substate => substate.get('pluginsFromMarketplace').toJS());
const makeSelectUuid = () => createSelector(selectAdminDomain(), substate => substate.get('uuid'));
const makeSelectAdmin = () => createSelector(selectAdminDomain(), substate => substate);
export default makeSelectAdmin;
export { makeSelectUuid, selectAdminDomain, makeSelectPluginsFromMarketplace };
export { selectAdminDomain };

View File

@ -22,6 +22,9 @@ describe('<Admin />', () => {
disableGlobalOverlayBlocker: jest.fn(),
emitEvent: jest.fn(),
enableGlobalOverlayBlocker: jest.fn(),
getUserPermissions: jest.fn(),
getUserPermissionsError: jest.fn(),
getUserPermissionsSucceeded: jest.fn(),
global: {
autoReload: false,
blockApp: false,

View File

@ -1,16 +1,21 @@
import { fromJS } from 'immutable';
import { setAppError } from '../actions';
import produce from 'immer';
import {
setAppError,
getUserPermissions,
getUserPermissionsError,
getUserPermissionsSucceeded,
} from '../actions';
import adminReducer from '../reducer';
describe('adminReducer', () => {
let state;
beforeEach(() => {
state = fromJS({
state = {
appError: false,
pluginsFromMarketplace: [],
});
isLoading: true,
userPermissions: [],
};
});
it('returns the initial state', () => {
@ -19,9 +24,39 @@ describe('adminReducer', () => {
expect(adminReducer(undefined, {})).toEqual(expected);
});
it('should handle the setaAppError action correctly', () => {
const expected = state.set('appError', true);
it('should handle the setAppError action correctly', () => {
const expected = produce(state, draft => {
draft.appError = true;
});
expect(adminReducer(state, setAppError())).toEqual(expected);
});
it('should handle the getUserPermissions action correctly', () => {
const expected = produce(state, draft => {
draft.isLoading = true;
});
expect(adminReducer(state, getUserPermissions())).toEqual(expected);
});
it('should handle the getUserPermissionsError action correctly', () => {
const error = 'Error';
const expected = produce(state, draft => {
draft.isLoading = false;
draft.error = error;
});
expect(adminReducer(state, getUserPermissionsError(error))).toEqual(expected);
});
it('should handle the getUserPermissionsSucceeded action correctly', () => {
const data = ['permission 1', 'permission 2'];
const expected = produce(state, draft => {
draft.isLoading = false;
draft.userPermissions = data;
});
expect(adminReducer(state, getUserPermissionsSucceeded(data))).toEqual(expected);
});
});

View File

@ -1,45 +0,0 @@
import { fromJS, Map } from 'immutable';
import makeSelectAdminDomain, { selectAdminDomain } from '../selectors';
describe('<Admin /> selectors', () => {
describe('selectAdminDomain selector', () => {
it('should select the global state', () => {
const state = fromJS({
autoReload: false,
appError: false,
currentEnvironment: 'development',
isLoading: true,
layout: Map({}),
showLeftMenu: true,
strapiVersion: '3',
uuid: false,
});
const mockedState = fromJS({
admin: state,
});
expect(selectAdminDomain()(mockedState)).toEqual(state);
});
});
describe('makeSelectAdminDomain', () => {
it('should select the global state (.toJS())', () => {
const state = fromJS({
autoReload: false,
appError: false,
currentEnvironment: 'development',
isLoading: true,
layout: Map({}),
showLeftMenu: true,
strapiVersion: '3',
uuid: false,
});
const mockedState = fromJS({
admin: state,
});
expect(makeSelectAdminDomain()(mockedState)).toEqual(state.toJS());
});
});
});

View File

@ -141,7 +141,7 @@ const LeftMenu = forwardRef(({ version, plugins }, ref) => {
getLinksPermissions();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [permissions]);
return (
<Wrapper>

View File

@ -56,7 +56,7 @@ const reducer = (state, action) =>
break;
}
case 'TOGGLE_IS_LOADING': {
draftState.isLoading = !state.isLoading;
draftState.isLoading = false;
break;
}
default:

View File

@ -14,6 +14,7 @@ const reducer = (state, action) =>
case 'GET_MODELS': {
draftState.collectionTypes = initialState.collectionTypes;
draftState.singleTypes = initialState.singleTypes;
draftState.components = initialState.components;
draftState.isLoading = true;
break;
}

View File

@ -1,6 +1,6 @@
import reducer from '../reducer';
describe('ADMIN | HOOKS | useContentTypes | reducer', () => {
describe('ADMIN | HOOKS | useModels | reducer', () => {
describe('DEFAULT_ACTION', () => {
it('should return the initialState', () => {
const state = {
@ -11,13 +11,14 @@ describe('ADMIN | HOOKS | useContentTypes | reducer', () => {
});
});
describe('GET_DATA_ERROR', () => {
describe('GET_MODELS_ERROR', () => {
it('should set isLoading to false is an error occured', () => {
const action = {
type: 'GET_MODELS_ERROR',
};
const initialState = {
collectionTypes: [],
components: [],
singleTypes: [
{
uid: 'app.homepage',
@ -31,6 +32,7 @@ describe('ADMIN | HOOKS | useContentTypes | reducer', () => {
};
const expected = {
collectionTypes: [],
components: [],
singleTypes: [],
isLoading: false,
};
@ -45,12 +47,37 @@ describe('ADMIN | HOOKS | useContentTypes | reducer', () => {
type: 'GET_MODELS',
};
const initialState = {
collectionTypes: [],
singleTypes: [],
isLoading: true,
collectionTypes: [
{
uid: 'app.category',
isDisplayed: true,
schema: {
kind: 'collectionType',
},
},
{
uid: 'app.category',
isDisplayed: true,
schema: {
kind: 'collectionType',
},
},
],
singleTypes: [
{
uid: 'app.homepage',
isDisplayed: true,
schema: {
kind: 'singleType',
},
},
],
components: [{}],
isLoading: false,
};
const expected = {
collectionTypes: [],
components: [],
singleTypes: [],
isLoading: true,
};
@ -63,7 +90,7 @@ describe('ADMIN | HOOKS | useContentTypes | reducer', () => {
it('should return the state with the collectionTypes and singleTypes', () => {
const action = {
type: 'GET_MODELS_SUCCEDED',
data: [
contentTypes: [
{
uid: 'app.homepage',
isDisplayed: true,
@ -86,9 +113,11 @@ describe('ADMIN | HOOKS | useContentTypes | reducer', () => {
},
},
],
components: [],
};
const initialState = {
collectionTypes: [],
components: [],
singleTypes: [],
isLoading: true,
};
@ -111,6 +140,7 @@ describe('ADMIN | HOOKS | useContentTypes | reducer', () => {
},
},
],
components: [],
isLoading: false,
};

View File

@ -21,82 +21,82 @@ const data = {
},
// Admin webhooks
{
action: 'admin::webhooks.create',
subject: null,
fields: null,
conditions: [],
},
{
action: 'admin::webhooks.read',
subject: null,
fields: null,
conditions: [],
},
{
action: 'admin::webhooks.update',
subject: null,
fields: null,
conditions: [],
},
{
action: 'admin::webhooks.delete',
subject: null,
fields: null,
conditions: [],
},
// {
// action: 'admin::webhooks.create',
// subject: null,
// fields: null,
// conditions: [],
// },
// {
// action: 'admin::webhooks.read',
// subject: null,
// fields: null,
// conditions: [],
// },
// {
// action: 'admin::webhooks.update',
// subject: null,
// fields: null,
// conditions: [],
// },
// {
// action: 'admin::webhooks.delete',
// subject: null,
// fields: null,
// conditions: [],
// },
// Admin users
{
action: 'admin::users.create',
subject: null,
fields: null,
conditions: [],
},
{
action: 'admin::users.read',
subject: null,
fields: null,
conditions: [],
},
{
action: 'admin::users.update',
subject: null,
fields: null,
conditions: [],
},
{
action: 'admin::users.delete',
subject: null,
fields: null,
conditions: [],
},
// {
// action: 'admin::users.create',
// subject: null,
// fields: null,
// conditions: [],
// },
// {
// action: 'admin::users.read',
// subject: null,
// fields: null,
// conditions: [],
// },
// {
// action: 'admin::users.update',
// subject: null,
// fields: null,
// conditions: [],
// },
// {
// action: 'admin::users.delete',
// subject: null,
// fields: null,
// conditions: [],
// },
// Admin roles
{
action: 'admin::roles.create',
subject: null,
fields: null,
conditions: [],
},
{
action: 'admin::roles.read',
subject: null,
fields: null,
conditions: [],
},
{
action: 'admin::roles.update',
subject: null,
fields: null,
conditions: [],
},
{
action: 'admin::roles.delete',
subject: null,
fields: null,
conditions: [],
},
// {
// action: 'admin::roles.create',
// subject: null,
// fields: null,
// conditions: [],
// },
// {
// action: 'admin::roles.read',
// subject: null,
// fields: null,
// conditions: [],
// },
// {
// action: 'admin::roles.update',
// subject: null,
// fields: null,
// conditions: [],
// },
// {
// action: 'admin::roles.delete',
// subject: null,
// fields: null,
// conditions: [],
// },
// Content type builder
{
@ -157,12 +157,12 @@ const data = {
fields: null,
conditions: null,
},
{
action: 'plugins::upload.settings.read',
subject: null,
fields: null,
conditions: null,
},
// {
// action: 'plugins::upload.settings.read',
// subject: null,
// fields: null,
// conditions: null,
// },
// Users-permissions
{
@ -331,6 +331,11 @@ const data = {
subject: 'application::address.address',
conditions: [],
},
{
action: 'plugins::content-manager.explorer.create',
subject: 'application::vegetable.vegetable',
conditions: [],
},
{
action: 'plugins::content-manager.explorer.create',
subject: 'application::restaurant.restaurant',

View File

@ -77,8 +77,7 @@ function serverRestartWatcher(response) {
if (res.status >= 400) {
throw new Error('not available');
}
// Hide the global OverlayBlocker
strapi.unlockApp();
resolve(response);
})
.catch(() => {
@ -146,9 +145,6 @@ export default function request(...args) {
.then(parseJSON)
.then(response => {
if (shouldWatchServerRestart) {
// Display the global OverlayBlocker
strapi.lockApp(shouldWatchServerRestart);
return serverRestartWatcher(response);
}

View File

@ -33,7 +33,14 @@ import {
const DataManagerProvider = ({ allIcons, children }) => {
const [reducerState, dispatch] = useReducer(reducer, initialState, init);
const [infoModals, toggleInfoModal] = useState({ cancel: false });
const { autoReload, currentEnvironment, emitEvent, formatMessage, menu } = useGlobalContext();
const {
autoReload,
currentEnvironment,
emitEvent,
fetchUserPermissions,
formatMessage,
menu,
} = useGlobalContext();
const {
components,
contentTypes,
@ -214,11 +221,18 @@ const DataManagerProvider = ({ allIcons, children }) => {
push({ search: '' });
if (userConfirm) {
strapi.lockApp();
await request(requestURL, { method: 'DELETE' }, true);
await updatePermissions();
// Reload the plugin so the cycle is new again
dispatch({ type: 'RELOAD_PLUGIN' });
// Refetch all the data
getDataRef.current();
strapi.unlockApp();
}
} catch (err) {
console.error({ err });
@ -252,15 +266,22 @@ const DataManagerProvider = ({ allIcons, children }) => {
return;
}
strapi.lockApp();
await request(requestURL, { method: 'DELETE' }, true);
// Reload the plugin so the cycle is new again
dispatch({ type: 'RELOAD_PLUGIN' });
// Refetch the permissions
await updatePermissions();
// Update the app menu
await updateAppMenu();
// Refetch all the data
getDataRef.current();
strapi.unlockApp();
}
} catch (err) {
console.error({ err });
@ -275,13 +296,20 @@ const DataManagerProvider = ({ allIcons, children }) => {
// Close the modal
push({ search: '' });
// Lock the app
strapi.lockApp();
// Update the category
await request(requestURL, { method: 'PUT', body }, true);
await updatePermissions();
// Reload the plugin so the cycle is new again
dispatch({ type: 'RELOAD_PLUGIN' });
// Refetch all the data
getDataRef.current();
strapi.unlockApp();
} catch (err) {
console.error({ err });
strapi.notification.error('notification.error');
@ -394,7 +422,13 @@ const DataManagerProvider = ({ allIcons, children }) => {
const baseURL = `/${pluginId}/${endPoint}`;
const requestURL = isCreating ? baseURL : `${baseURL}/${currentUid}`;
// Lock the app
strapi.lockApp();
await request(requestURL, { method, body }, true);
await updatePermissions();
// Update the app menu
await updateAppMenu();
@ -416,6 +450,8 @@ const DataManagerProvider = ({ allIcons, children }) => {
dispatch({ type: 'RELOAD_PLUGIN' });
// Refetch all the data
getDataRef.current();
strapi.unlockApp();
} catch (err) {
if (!isInContentTypeView) {
emitEvent('didNotSaveComponent');
@ -437,6 +473,10 @@ const DataManagerProvider = ({ allIcons, children }) => {
}
};
const updatePermissions = async () => {
await fetchUserPermissions('user2');
};
const updateSchema = (data, schemaType, componentUID) => {
dispatch({
type: 'UPDATE_SCHEMA',