Merge pull request #10655 from strapi/core/admin-customisations

[Core] Enable admin panel customisations
This commit is contained in:
cyril lopez 2021-07-27 11:18:32 +02:00 committed by GitHub
commit c2ae41acb0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 408 additions and 130 deletions

View File

@ -36,8 +36,6 @@ module.exports = {
BACKEND_URL: true,
PUBLIC_PATH: true,
NODE_ENV: true,
STRAPI_ADMIN_SHOW_TUTORIALS: true,
STRAPI_ADMIN_UPDATE_NOTIFICATION: true,
},
settings: {
react: {

View File

@ -1,5 +1,6 @@
const frontPaths = [
'packages/**/admin/src/**/*.js',
'packages/generators/app/lib/resources/files/admin/app.js',
'packages/**/ee/admin/**/*.js',
'packages/core/helper-plugin/**/*.js',
'packages/**/tests/front/**/*.js',

View File

@ -1,15 +0,0 @@
'use strict';
module.exports = {
webpack: (config, webpack) => {
// Note: we provide webpack above so you should not `require` it
// Perform customizations to webpack config
// Important: return the modified config
return config;
},
app: config => {
config.locales = ['fr'];
return config;
},
};

View File

@ -0,0 +1,33 @@
// import MyCompo from './extensions/MyCompo';
export default {
config: {
// Leaving this commented on purpose
// auth: {
// logo:
// 'https://images.unsplash.com/photo-1593642634367-d91a135587b5?ixid=MnwxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=750&q=80',
// },
// head: {
// favicon:
// 'https://images.unsplash.com/photo-1593642634367-d91a135587b5?ixid=MnwxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=750&q=80',
// title: 'Strapi',
// },
// locales: ['fr', 'toto'],
// menu: {
// logo: null,
// },
// theme: {
// main: {
// colors: { ok: 't' },
// },
// },
// translations: {
// fr: {
// 'Auth.form.email.label': 'test',
// },
// },
// tutorials: false,
// notifications: { release: false },
},
bootstrap() {},
};

View File

@ -45,8 +45,7 @@ module.exports = {
BACKEND_URL: 'http://localhost:1337',
ADMIN_PATH: '/admin',
NODE_ENV: 'test',
'process.env.STRAPI_ADMIN_SHOW_TUTORIALS': 'false',
'process.env.STRAPI_ADMIN_UPDATE_NOTIFICATION': 'false',
// FIXME create a clean config file
},
moduleDirectories: [

View File

@ -1,2 +0,0 @@
STRAPI_ADMIN_SHOW_TUTORIALS=false
STRAPI_ADMIN_UPDATE_NOTIFICATION=false

View File

@ -1,11 +1,16 @@
import React from 'react';
import { BrowserRouter } from 'react-router-dom';
import merge from 'lodash/merge';
import pick from 'lodash/pick';
import isFunction from 'lodash/isFunction';
import invariant from 'invariant';
import { Helmet } from 'react-helmet';
import { basename, createHook } from './core/utils';
import configureStore from './core/store/configureStore';
import { Plugin } from './core/apis';
import App from './pages/App';
import AuthLogo from './assets/images/logo_strapi_auth.png';
import MenuLogo from './assets/images/logo_strapi_menu.png';
import Providers from './components/Providers';
import Theme from './components/Theme';
import languageNativeNames from './translations/languageNativeNames';
@ -16,11 +21,23 @@ import {
MUTATE_SINGLE_TYPES_LINKS,
} from './exposedHooks';
import injectionZones from './injectionZones';
import favicon from './favicon.ico';
import themes from './themes';
class StrapiApp {
constructor({ appPlugins, library, locales, middlewares, reducers }) {
this.appLocales = ['en', ...locales.filter(loc => loc !== 'en')];
constructor({ adminConfig, appPlugins, library, middlewares, reducers }) {
this.customConfigurations = adminConfig.config;
this.customBootstrapConfiguration = adminConfig.bootstrap;
this.configurations = {
authLogo: AuthLogo,
head: { favicon },
locales: ['en'],
menuLogo: MenuLogo,
notifications: { releases: true },
theme: themes,
translations: {},
tutorials: true,
};
this.appPlugins = appPlugins || {};
this.library = library;
this.middlewares = middlewares;
@ -155,19 +172,66 @@ class StrapiApp {
});
}
});
if (isFunction(this.customBootstrapConfiguration)) {
this.customBootstrapConfiguration({
addComponents: this.addComponents,
addFields: this.addFields,
addMenuLink: this.addMenuLink,
addReducers: this.addReducers,
addSettingsLink: this.addSettingsLink,
addSettingsLinks: this.addSettingsLinks,
getPlugin: this.getPlugin,
injectContentManagerComponent: this.injectContentManagerComponent,
registerHook: this.registerHook,
});
}
}
bootstrapAdmin = async () => {
await this.createCustomConfigurations();
this.createHook(INJECT_COLUMN_IN_TABLE);
this.createHook(MUTATE_COLLECTION_TYPES_LINKS);
this.createHook(MUTATE_SINGLE_TYPES_LINKS);
this.createHook(MUTATE_EDIT_VIEW_LAYOUT);
await this.loadAdminTrads();
return Promise.resolve();
};
createCustomConfigurations = async () => {
if (this.customConfigurations?.locales) {
this.configurations.locales = [
'en',
...this.customConfigurations.locales?.filter(loc => loc !== 'en'),
];
}
if (this.customConfigurations?.auth?.logo) {
this.configurations.authLogo = this.customConfigurations.auth.logo;
}
if (this.customConfigurations?.menu?.logo) {
this.configurations.menuLogo = this.customConfigurations.menu.logo;
}
if (this.customConfigurations?.head?.favicon) {
this.configurations.head.favicon = this.customConfigurations.head.favicon;
}
if (this.customConfigurations?.theme) {
this.configurations.theme = merge(this.configurations.theme, this.customConfigurations.theme);
}
if (this.customConfigurations?.notifications?.releases !== undefined) {
this.configurations.notifications.releases = this.customConfigurations.notifications.releases;
}
if (this.customConfigurations?.tutorials !== undefined) {
this.configurations.tutorials = this.customConfigurations.tutorials;
}
};
createHook = name => {
this.hooksDict[name] = createHook();
};
@ -235,34 +299,47 @@ class StrapiApp {
this.admin.injectionZones.contentManager[containerName][blockName].push(component);
};
/**
* Load the admin translations
* @returns {Object} The imported admin translations
*/
async loadAdminTrads() {
const arrayOfPromises = this.appLocales.map(locale => {
const arrayOfPromises = this.configurations.locales.map(locale => {
return import(/* webpackChunkName: "[request]" */ `./translations/${locale}.json`)
.then(({ default: data }) => {
return { data, locale };
})
.catch(() => {
return { data: {}, locale };
return { data: null, locale };
});
});
const adminLocales = await Promise.all(arrayOfPromises);
this.translations = adminLocales.reduce((acc, current) => {
acc[current.locale] = current.data;
const translations = adminLocales.reduce((acc, current) => {
if (current.data) {
acc[current.locale] = current.data;
}
return acc;
}, {});
return Promise.resolve();
return translations;
}
/**
* Load the application's translations and merged the custom translations
* with the default ones.
*
*/
async loadTrads() {
const adminTranslations = await this.loadAdminTrads();
const arrayOfPromises = Object.keys(this.appPlugins)
.map(plugin => {
const registerTrads = this.appPlugins[plugin].registerTrads;
if (registerTrads) {
return registerTrads({ locales: this.appLocales });
return registerTrads({ locales: this.configurations.locales });
}
return null;
@ -284,15 +361,18 @@ class StrapiApp {
return acc;
}, {});
this.translations = Object.keys(this.translations).reduce((acc, current) => {
const translations = this.configurations.locales.reduce((acc, current) => {
acc[current] = {
...this.translations[current],
...adminTranslations[current],
...(mergedTrads[current] || {}),
...this.customConfigurations?.translations?.[current],
};
return acc;
}, {});
this.configurations.translations = translations;
return Promise.resolve();
}
@ -323,7 +403,7 @@ class StrapiApp {
render() {
const store = this.createStore();
const localeNames = pick(languageNativeNames, this.appLocales);
const localeNames = pick(languageNativeNames, this.configurations.locales || []);
const {
components: { components },
@ -331,15 +411,17 @@ class StrapiApp {
} = this.library;
return (
<Theme theme={themes}>
<Theme theme={this.configurations.theme}>
<Providers
authLogo={this.configurations.authLogo}
components={components}
fields={fields}
localeNames={localeNames}
getAdminInjectedComponents={this.getAdminInjectedComponents}
getPlugin={this.getPlugin}
messages={this.translations}
messages={this.configurations.translations}
menu={this.menu}
menuLogo={this.configurations.menuLogo}
plugins={this.plugins}
runHookParallel={this.runHookParallel}
runHookWaterfall={(name, initialValue, async = false) => {
@ -347,16 +429,29 @@ class StrapiApp {
}}
runHookSeries={this.runHookSeries}
settings={this.settings}
showTutorials={this.configurations.tutorials}
showReleaseNotification={this.configurations.notifications.releases}
store={store}
>
<BrowserRouter basename={basename}>
<App store={store} />
</BrowserRouter>
<>
<Helmet
link={[
{
rel: 'icon',
type: 'image/png',
href: this.configurations.head.favicon,
},
]}
/>
<BrowserRouter basename={basename}>
<App store={store} />
</BrowserRouter>
</>
</Providers>
</Theme>
);
}
}
export default ({ appPlugins, library, locales, middlewares, reducers }) =>
new StrapiApp({ appPlugins, library, locales, middlewares, reducers });
export default ({ adminConfig = {}, appPlugins, library, middlewares, reducers }) =>
new StrapiApp({ adminConfig, appPlugins, library, middlewares, reducers });

View File

@ -1,7 +0,0 @@
module.exports = {
app: config => {
config.locales = ['fr'];
return config;
},
};

View File

@ -0,0 +1,9 @@
export default {
config: {},
bootstrap(app) {
app.injectContentManagerComponent('editView', 'informations', {
name: 'i18n-locale-filter-edit-view',
Component: () => 'test',
});
},
};

View File

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -2,16 +2,16 @@ import React, { useMemo } from 'react';
import { LoadingIndicatorPage, AppInfosContext } from '@strapi/helper-plugin';
import { useQueries } from 'react-query';
import packageJSON from '../../../../package.json';
import { useConfigurations } from '../../hooks';
import PluginsInitializer from '../PluginsInitializer';
import RBACProvider from '../RBACProvider';
import { fetchAppInfo, fetchCurrentUserPermissions, fetchStrapiLatestRelease } from './utils/api';
import checkLatestStrapiVersion from './utils/checkLatestStrapiVersion';
const { STRAPI_ADMIN_UPDATE_NOTIFICATION } = process.env;
const canFetchRelease = STRAPI_ADMIN_UPDATE_NOTIFICATION === 'true';
const strapiVersion = packageJSON.version;
const AuthenticatedApp = () => {
const { showReleaseNotification } = useConfigurations();
const [
{ data: appInfos, status },
{ data: tag_name, isLoading },
@ -21,7 +21,7 @@ const AuthenticatedApp = () => {
{
queryKey: 'strapi-release',
queryFn: fetchStrapiLatestRelease,
enabled: canFetchRelease,
enabled: showReleaseNotification,
initialData: strapiVersion,
},
{

View File

@ -1,6 +1,7 @@
import React from 'react';
import { render } from '@testing-library/react';
import { QueryClientProvider, QueryClient } from 'react-query';
import { ConfigurationsContext } from '../../../contexts';
import { fetchAppInfo, fetchCurrentUserPermissions, fetchStrapiLatestRelease } from '../utils/api';
import packageJSON from '../../../../../package.json';
import AuthenticatedApp from '..';
@ -27,7 +28,9 @@ const queryClient = new QueryClient({
const app = (
<QueryClientProvider client={queryClient}>
<AuthenticatedApp />
<ConfigurationsContext.Provider value={{ showReleaseNotification: false }}>
<AuthenticatedApp />
</ConfigurationsContext.Provider>
</QueryClientProvider>
);

View File

@ -1,8 +1,6 @@
import styled from 'styled-components';
import PropTypes from 'prop-types';
import Logo from '../../../../assets/images/logo-strapi.png';
const Wrapper = styled.div`
background-color: #007eff;
padding-left: 2rem;
@ -22,7 +20,7 @@ const Wrapper = styled.div`
letter-spacing: 0.2rem;
color: $white;
background-image: url(${Logo});
background-image: url(${props => props.logo});
background-repeat: no-repeat;
background-position: left center;
background-size: auto 2.5rem;

View File

@ -1,14 +1,18 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { useConfigurations } from '../../../../hooks';
import Wrapper from './Wrapper';
const LeftMenuHeader = () => (
<Wrapper>
<Link to="/" className="leftMenuHeaderLink">
<span className="projectName" />
</Link>
</Wrapper>
);
const LeftMenuHeader = () => {
const { menuLogo } = useConfigurations();
return (
<Wrapper logo={menuLogo}>
<Link to="/" className="leftMenuHeaderLink">
<span className="projectName" />
</Link>
</Wrapper>
);
};
export default LeftMenuHeader;

View File

@ -5,6 +5,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faQuestion, faTimes } from '@fortawesome/free-solid-svg-icons';
import cn from 'classnames';
import { useTracking } from '@strapi/helper-plugin';
import { useConfigurations } from '../../hooks';
import formatVideoArray from './utils/formatAndStoreVideoArray';
import StaticLinks from './StaticLinks';
import Video from './Video';
@ -12,7 +14,9 @@ import Wrapper from './Wrapper';
import reducer, { initialState } from './reducer';
const Onboarding = () => {
if (process.env.STRAPI_ADMIN_SHOW_TUTORIALS !== 'true') {
const { showTutorials } = useConfigurations();
if (!showTutorials) {
return null;
}
@ -21,6 +25,7 @@ const Onboarding = () => {
const OnboardingVideos = () => {
const { trackUsage } = useTracking();
const [{ isLoading, isOpen, videos }, dispatch] = useReducer(reducer, initialState);
useEffect(() => {

View File

@ -2,10 +2,8 @@ import React, { memo } from 'react';
import { Helmet } from 'react-helmet';
import PropTypes from 'prop-types';
import favicon from '../../favicon.ico';
const PageTitle = ({ title }) => {
return <Helmet title={title} link={[{ rel: 'icon', type: 'image/png', href: favicon }]} />;
return <Helmet title={title} />;
};
PageTitle.propTypes = {

View File

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { QueryClientProvider, QueryClient } from 'react-query';
import { LibraryProvider, StrapiAppProvider } from '@strapi/helper-plugin';
import { Provider } from 'react-redux';
import { AdminContext } from '../../contexts';
import { AdminContext, ConfigurationsContext } from '../../contexts';
import LanguageProvider from '../LanguageProvider';
import AutoReloadOverlayBlockerProvider from '../AutoReloadOverlayBlockerProvider';
import Notifications from '../Notifications';
@ -18,6 +18,7 @@ const queryClient = new QueryClient({
});
const Providers = ({
authLogo,
children,
components,
fields,
@ -25,37 +26,45 @@ const Providers = ({
getPlugin,
localeNames,
menu,
menuLogo,
messages,
plugins,
runHookParallel,
runHookSeries,
runHookWaterfall,
settings,
showReleaseNotification,
showTutorials,
store,
}) => {
return (
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<AdminContext.Provider value={{ getAdminInjectedComponents }}>
<StrapiAppProvider
getPlugin={getPlugin}
menu={menu}
plugins={plugins}
runHookParallel={runHookParallel}
runHookWaterfall={runHookWaterfall}
runHookSeries={runHookSeries}
settings={settings}
<ConfigurationsContext.Provider
value={{ authLogo, menuLogo, showReleaseNotification, showTutorials }}
>
<LibraryProvider components={components} fields={fields}>
<LanguageProvider messages={messages} localeNames={localeNames}>
<AutoReloadOverlayBlockerProvider>
<OverlayBlocker>
<Notifications>{children}</Notifications>
</OverlayBlocker>
</AutoReloadOverlayBlockerProvider>
</LanguageProvider>
</LibraryProvider>
</StrapiAppProvider>
<StrapiAppProvider
getPlugin={getPlugin}
menu={menu}
plugins={plugins}
runHookParallel={runHookParallel}
runHookWaterfall={runHookWaterfall}
runHookSeries={runHookSeries}
settings={settings}
>
<LibraryProvider components={components} fields={fields}>
<LanguageProvider messages={messages} localeNames={localeNames}>
<AutoReloadOverlayBlockerProvider>
<OverlayBlocker>
<Notifications>{children}</Notifications>
</OverlayBlocker>
</AutoReloadOverlayBlockerProvider>
</LanguageProvider>
</LibraryProvider>
</StrapiAppProvider>
</ConfigurationsContext.Provider>
</AdminContext.Provider>
</Provider>
</QueryClientProvider>
@ -63,6 +72,7 @@ const Providers = ({
};
Providers.propTypes = {
authLogo: PropTypes.oneOfType([PropTypes.string, PropTypes.any]).isRequired,
children: PropTypes.element.isRequired,
components: PropTypes.object.isRequired,
fields: PropTypes.object.isRequired,
@ -81,12 +91,15 @@ Providers.propTypes = {
Component: PropTypes.func,
})
).isRequired,
menuLogo: PropTypes.oneOfType([PropTypes.string, PropTypes.any]).isRequired,
messages: PropTypes.object.isRequired,
plugins: PropTypes.object.isRequired,
runHookParallel: PropTypes.func.isRequired,
runHookWaterfall: PropTypes.func.isRequired,
runHookSeries: PropTypes.func.isRequired,
settings: PropTypes.object.isRequired,
showReleaseNotification: PropTypes.bool.isRequired,
showTutorials: PropTypes.bool.isRequired,
store: PropTypes.object.isRequired,
};

View File

@ -1,4 +1,4 @@
import { permissions } from './data';
import testData, { permissions } from './data';
export { default as testData } from './data';
export { testData };
export { permissions };

View File

@ -0,0 +1,5 @@
import { createContext } from 'react';
const ConfigurationsContext = createContext({});
export default ConfigurationsContext;

View File

@ -1,2 +1,3 @@
export { default as AdminContext } from './Admin';
export { default as ConfigurationsContext } from './Configurations';
export { default as PermissionsDataManagerContext } from './PermisssionsDataManagerContext';

View File

@ -1,3 +1,4 @@
export { default as useConfigurations } from './useConfigurations';
export { default as useModels } from './useModels';
export { default as useFetchPermissionsLayout } from './useFetchPermissionsLayout';
export { default as useFetchPluginsFromMarketPlace } from './useFetchPluginsFromMarketPlace';

View File

@ -0,0 +1,10 @@
import { useContext } from 'react';
import { ConfigurationsContext } from '../../contexts';
const useConfigurations = () => {
const context = useContext(ConfigurationsContext);
return context;
};
export default useConfigurations;

View File

@ -1,7 +1,7 @@
import ReactDOM from 'react-dom';
import { Components, Fields, Middlewares, Reducers } from './core/apis';
import { axiosInstance } from './core/utils';
import appCustomisations from './admin.config';
import appCustomisations from './app';
import plugins from './plugins';
import appReducers from './reducers';
@ -14,11 +14,7 @@ window.strapi = {
projectType: 'Community',
};
const appConfig = {
locales: [],
};
const customConfig = appCustomisations.app(appConfig);
const customConfig = appCustomisations;
const library = {
components: Components(),
@ -56,7 +52,8 @@ const run = async () => {
const app = StrapiApp.default({
appPlugins: plugins,
library,
locales: customConfig.locales,
adminConfig: customConfig,
bootstrap: customConfig,
middlewares,
reducers,
});
@ -64,6 +61,7 @@ const run = async () => {
await app.bootstrapAdmin();
await app.initialize();
await app.bootstrap();
await app.loadTrads();
ReactDOM.render(app.render(), MOUNT_NODE);

View File

@ -20,6 +20,7 @@ jest.mock('../../../hooks', () => ({
useMenu: jest.fn(() => ({ isLoading: true, generalSectionLinks: [], pluginsSectionLinks: [] })),
useTrackUsage: jest.fn(),
useReleaseNotification: jest.fn(),
useConfigurations: jest.fn(() => ({ showTutorials: false })),
}));
jest.mock('../../../components/LeftMenu', () => () => <div>menu</div>);

View File

@ -1,7 +1,11 @@
import React from 'react';
import LogoStrapi from '../../../../assets/images/logo_strapi.png';
import { useConfigurations } from '../../../../hooks';
import Img from './Img';
const Logo = () => <Img src={LogoStrapi} alt="strapi-logo" />;
const Logo = () => {
const { authLogo } = useConfigurations();
return <Img src={authLogo} alt="strapi" />;
};
export default Logo;

View File

@ -7,11 +7,10 @@ import appReducers from '../reducers';
const library = { fields: Fields(), components: Components() };
const middlewares = { middlewares: [] };
const reducers = { reducers: appReducers };
const locales = [];
describe('ADMIN | StrapiApp', () => {
it('should render the app without plugins', () => {
const app = StrapiApp({ middlewares, reducers, library, locales });
const app = StrapiApp({ middlewares, reducers, library });
const { container } = render(app.render());
expect(container.firstChild).toMatchInlineSnapshot(`
@ -45,7 +44,7 @@ describe('ADMIN | StrapiApp', () => {
});
it('should create a valid store', () => {
const app = StrapiApp({ middlewares, reducers, library, locales });
const app = StrapiApp({ middlewares, reducers, library });
const store = app.createStore();
@ -54,7 +53,7 @@ describe('ADMIN | StrapiApp', () => {
describe('Hook api', () => {
it('runs the "moto" hooks in series', () => {
const app = StrapiApp({ middlewares, reducers, library, locales });
const app = StrapiApp({ middlewares, reducers, library });
app.createHook('hello');
app.createHook('moto');
@ -72,7 +71,7 @@ describe('ADMIN | StrapiApp', () => {
});
it('runs the "moto" hooks in series asynchronously', async () => {
const app = StrapiApp({ middlewares, reducers, library, locales });
const app = StrapiApp({ middlewares, reducers, library });
app.createHook('hello');
app.createHook('moto');
@ -90,7 +89,7 @@ describe('ADMIN | StrapiApp', () => {
});
it('runs the "moto" hooks in waterfall', () => {
const app = StrapiApp({ middlewares, reducers, library, locales });
const app = StrapiApp({ middlewares, reducers, library });
app.createHook('hello');
app.createHook('moto');
@ -106,7 +105,7 @@ describe('ADMIN | StrapiApp', () => {
});
it('runs the "moto" hooks in waterfall asynchronously', async () => {
const app = StrapiApp({ middlewares, reducers, library, locales });
const app = StrapiApp({ middlewares, reducers, library });
app.createHook('hello');
app.createHook('moto');
@ -122,7 +121,7 @@ describe('ADMIN | StrapiApp', () => {
});
it('runs the "moto" hooks in parallel', async () => {
const app = StrapiApp({ middlewares, reducers, library, locales });
const app = StrapiApp({ middlewares, reducers, library });
app.createHook('hello');
app.createHook('moto');
@ -142,14 +141,14 @@ describe('ADMIN | StrapiApp', () => {
describe('Settings api', () => {
it('the settings should be defined', () => {
const app = StrapiApp({ middlewares, reducers, library, locales });
const app = StrapiApp({ middlewares, reducers, library });
expect(app.settings).toBeDefined();
expect(app.settings.global).toBeDefined();
});
it('should creates a new section', () => {
const app = StrapiApp({ middlewares, reducers, library, locales });
const app = StrapiApp({ middlewares, reducers, library });
const section = { id: 'foo', intlLabel: { id: 'foo', defaultMessage: 'foo' } };
const links = [
{
@ -166,7 +165,7 @@ describe('ADMIN | StrapiApp', () => {
});
it('should add a link correctly to the global section', () => {
const app = StrapiApp({ middlewares, reducers, library, locales });
const app = StrapiApp({ middlewares, reducers, library });
const link = {
Component: jest.fn(),
to: '/bar',
@ -181,7 +180,7 @@ describe('ADMIN | StrapiApp', () => {
});
it('should add an array of links correctly to the global section', () => {
const app = StrapiApp({ middlewares, reducers, library, locales });
const app = StrapiApp({ middlewares, reducers, library });
const links = [
{
Component: jest.fn(),
@ -200,14 +199,14 @@ describe('ADMIN | StrapiApp', () => {
describe('Menu api', () => {
it('the menu should be defined', () => {
const app = StrapiApp({ middlewares, reducers, library, locales });
const app = StrapiApp({ middlewares, reducers, library });
expect(app.menu).toBeDefined();
expect(Array.isArray(app.menu)).toBe(true);
});
it('addMenuLink should add a link to the menu', () => {
const app = StrapiApp({ middlewares, reducers, library, locales });
const app = StrapiApp({ middlewares, reducers, library });
const link = {
Component: jest.fn(),
to: '/plugins/bar',
@ -223,7 +222,7 @@ describe('ADMIN | StrapiApp', () => {
});
it('addCorePluginMenuLink should add a link to the menu', () => {
const app = StrapiApp({ middlewares, reducers, library, locales });
const app = StrapiApp({ middlewares, reducers, library });
const link = {
to: '/plugins/content-type-builder',
icon: 'book',
@ -240,4 +239,83 @@ describe('ADMIN | StrapiApp', () => {
expect(app.menu[0]).toEqual(link);
});
});
describe('createCustomConfigurations', () => {
it('should add a locale', () => {
const adminConfig = {
config: { locales: ['fr'] },
};
const app = StrapiApp({ middlewares, reducers, library, adminConfig });
app.createCustomConfigurations();
expect(app.configurations.locales).toEqual(['en', 'fr']);
});
it('should override the authLogo', () => {
const adminConfig = {
config: { auth: { logo: 'fr' } },
};
const app = StrapiApp({ middlewares, reducers, library, adminConfig });
app.createCustomConfigurations();
expect(app.configurations.authLogo).toBe('fr');
});
it('should override the menuLogo', () => {
const adminConfig = {
config: { menu: { logo: 'fr' } },
};
const app = StrapiApp({ middlewares, reducers, library, adminConfig });
app.createCustomConfigurations();
expect(app.configurations.menuLogo).toBe('fr');
});
it('should override the favicon', () => {
const adminConfig = {
config: { head: { favicon: 'fr' } },
};
const app = StrapiApp({ middlewares, reducers, library, adminConfig });
app.createCustomConfigurations();
expect(app.configurations.head.favicon).toBe('fr');
});
it('should override the theme', () => {
const adminConfig = {
config: { theme: { main: { colors: { red: 'black' } } } },
};
const app = StrapiApp({ middlewares, reducers, library, adminConfig });
app.createCustomConfigurations();
expect(app.configurations.theme.main.colors.red).toBe('black');
});
it('should override the tutorials', () => {
const adminConfig = {
config: { tutorials: false },
};
const app = StrapiApp({ middlewares, reducers, library, adminConfig });
app.createCustomConfigurations();
expect(app.configurations.tutorials).toBeFalsy();
});
it('should override the release notification', () => {
const adminConfig = {
config: { notifications: { releases: false } },
};
const app = StrapiApp({ middlewares, reducers, library, adminConfig });
app.createCustomConfigurations();
expect(app.configurations.notifications.releases).toBeFalsy();
});
});
});

View File

@ -25,8 +25,6 @@ const getClientEnvironment = options => {
ADMIN_PATH: options.adminPath,
NODE_ENV: options.env || 'development',
STRAPI_ADMIN_BACKEND_URL: options.backend,
STRAPI_ADMIN_SHOW_TUTORIALS: 'true',
STRAPI_ADMIN_UPDATE_NOTIFICATION: 'true',
}
);

View File

@ -6,6 +6,7 @@ const fs = require('fs-extra');
const webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const chalk = require('chalk');
const chokidar = require('chokidar');
const getWebpackConfig = require('./webpack.config');
const getPkgPath = name => path.dirname(require.resolve(`${name}/package.json`));
@ -186,14 +187,18 @@ async function createCacheDir(dir) {
// copy plugins code
await Promise.all(pluginsToCopy.map(name => copyPlugin(name, cacheDir)));
// Copy admin.config.js
const customAdminConfigFilePath = path.join(dir, 'admin', 'admin.config.js');
// Copy app.js
const customAdminConfigFilePath = path.join(dir, 'admin', 'app.js');
if (fs.existsSync(customAdminConfigFilePath)) {
await fs.copy(
customAdminConfigFilePath,
path.resolve(cacheDir, 'admin', 'src', 'admin.config.js')
);
await fs.copy(customAdminConfigFilePath, path.resolve(cacheDir, 'admin', 'src', 'app.js'));
}
// Copy admin extensions folder
const adminExtensionFolder = path.join(dir, 'admin', 'extensions');
if (fs.existsSync(adminExtensionFolder)) {
await fs.copy(adminExtensionFolder, path.resolve(cacheDir, 'admin', 'src', 'extensions'));
}
// create plugins.js with plugins requires
@ -248,6 +253,57 @@ async function watchAdmin({ dir, host, port, browser, options }) {
console.log();
console.log(chalk.green(`Admin development at http://${host}:${port}${opts.publicPath}`));
});
watchFiles(dir);
}
/**
* Listen to files change and copy the changed files in the .cache/admin folder
* when using the dev mode
* @param {string} dir
*/
async function watchFiles(dir) {
await createCacheDir(dir);
const cacheDir = path.join(dir, '.cache');
const appExtensionFile = path.join(dir, 'admin', 'app.js');
const extensionsPath = path.join(dir, 'admin', 'extensions');
// Only watch the admin/app.js file and the files that are in the ./admin/extensions/folder
const filesToWatch = [appExtensionFile, extensionsPath];
const watcher = chokidar.watch(filesToWatch, {
ignoreInitial: true,
ignorePermissionErrors: true,
});
watcher.on('all', async (event, filePath) => {
const isAppFile = filePath.includes(appExtensionFile);
// The app.js file needs to be copied in the .cache/admin/src/app.js and the other ones needs to
// be copied in the .cache/admin/src/extensions folder
const targetPath = isAppFile
? path.join(path.normalize(filePath.split(appExtensionFile)[1]), 'app.js')
: path.join('extensions', path.normalize(filePath.split(extensionsPath)[1]));
const destFolder = path.join(cacheDir, 'admin', 'src');
if (event === 'unlink' || event === 'unlinkDir') {
// Remove the file or folder
// We need to copy the original files when deleting an override one
try {
fs.removeSync(path.join(destFolder, targetPath));
} catch (err) {
console.log('An error occured while deleting the file', err);
}
} else {
// In any other case just copy the file into the .cache/admin/src folder
try {
await fs.copy(filePath, path.join(destFolder, targetPath));
} catch (err) {
console.log(err);
}
}
});
}
module.exports = {

View File

@ -1,10 +0,0 @@
'use strict';
/* eslint-disable no-unused-vars */
module.exports = {
app: config => {
config.locales = ['fr'];
return config;
},
};

View File

@ -0,0 +1,4 @@
export default {
config: {},
bootstrap() {},
};