Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

External menu configurations #289

Merged
merged 20 commits into from
Jan 18, 2019
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
41d6a5c
refactor(navbar): render menu items based on fetched configuration
emmenko Jan 11, 2019
d30e409
refactor(navbar): rename query
emmenko Jan 13, 2019
c635bc4
test(navbar): fix setup
emmenko Jan 14, 2019
f0a9465
refactor(user-settings-menu): to render links from appbar menu config
emmenko Jan 14, 2019
ba20c6a
chore(i18n): update i18n messages
emmenko Jan 14, 2019
285e69c
refactor(user-settings-menu): use cache-only fetch policy
emmenko Jan 14, 2019
978340d
refactor(apollo): revert to one client, pass custom uri to query context
emmenko Jan 14, 2019
23e541c
refactor: load menu configs from local menu.json files (dev mode only)
emmenko Jan 15, 2019
773f95f
fix(app-shell/test-utils): import
emmenko Jan 15, 2019
f8d2a89
chore: remove test api url
emmenko Jan 15, 2019
526836a
chore: remove comment [skip ci]
emmenko Jan 15, 2019
bbe6565
refactor(app-shell): to have withApplicationsMenu HOC which condition…
emmenko Jan 15, 2019
818d63e
chore: update app-shell readme, small changes / fixes
emmenko Jan 15, 2019
4d8a102
feat(mc-scripts/dev-server): add fake graphql endpoint to return erro…
emmenko Jan 15, 2019
e8c5250
test: add tests for withApplicationsMenu
emmenko Jan 15, 2019
1dbdd29
fix: pass menu loader only if NODE_ENV is development
emmenko Jan 16, 2019
3b4ebc7
refactor(apollo): send cookies only for requests to MC backend API
emmenko Jan 16, 2019
4526ff6
refactor: keep it simple
emmenko Jan 16, 2019
53e5066
test(apollo): update snapshots
emmenko Jan 16, 2019
c98e509
feat: add handleApolloErrors HOC to automatically dispatch query erro…
emmenko Jan 17, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions application-templates/starter/menu.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"key": "examples-starter",
"uriPath": "examples-starter",
"icon": "RocketIcon",
"permissions": ["ViewProducts", "ManageProducts"],
"featureToggle": null,
"submenu": [],
"labelAllLocales": [
{
"locale": "en",
"value": "Starter"
},
{
"locale": "de",
"value": "Starter"
},
{
"locale": "es",
"value": "Starter"
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,10 @@ import { Sdk } from '@commercetools-frontend/sdk';
import * as globalActions from '@commercetools-frontend/actions-global';
import { Redirect, Route, Switch } from 'react-router-dom';

const loadApplicationMessagesForLanguage = lang =>
new Promise((resolve, reject) =>
import(`../../i18n/data/${lang}.json` /* webpackChunkName: "application-messages-[request]" */).then(
response => {
resolve(response.default);
},
error => {
reject(error);
}
)
);
const loadApplicationMessagesForLanguage = async lang => {
const messages = await import(`../../i18n/data/${lang}.json` /* webpackChunkName: "application-messages-[request]" */);
return messages.default;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated refactoring, correct?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes


// Here we split up the main (app) bundle with the actual application business logic.
// Splitting by route is usually recommended and you can potentially have a splitting
Expand Down Expand Up @@ -62,6 +55,7 @@ class EntryPoint extends React.Component {
globalActions.handleActionError(error, 'sdk')(dispatch);
}}
applicationMessages={loadApplicationMessagesForLanguage}
DEV_ONLY__loadNavbarMenuConfig={() => import('../../../menu.json')}
render={() => <ApplicationStarter />}
/>
);
Expand Down
88 changes: 17 additions & 71 deletions packages/application-shell/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,92 +18,38 @@ $ npm install --save @commercetools-frontend/application-shell

## Usage

```js
/**
* This is the entry point of an application.
* See `@commercetools-frontend/application-shell` for usage.
*/
import React from 'react';
import { Provider as StoreProvider } from 'react-redux';
import {
ApplicationShell,
reduxStore,
setupGlobalErrorListener,
} from '@commercetools-frontend/application-shell';
import { Sdk } from '@commercetools-frontend/sdk';
import * as globalActions from '@commercetools-frontend/actions-global';
import PageNotFound from '@commercetools-local/core/components/page-not-found';
import applicationMessages from '../../i18n';

import trackingEventWhitelist from './tracking-whitelist';

// Ensure to setup the global error listener before any React component renders
// in order to catch possible errors on rendering/mounting.
setupGlobalErrorListener(reduxStore.dispatch);

const EntryPoint = () => (
<StoreProvider store={reduxStore}>
<ApplicationShell
applicationMessages={applicationMessages}
configuration={window.app}
trackingEventWhitelist={trackingEventWhitelist}
onRegisterErrorListeners={() => {
Sdk.Get.errorHandler = error =>
handleActionError(error, 'sdk')(reduxStore.dispatch);
}}
render={() => (
<Switch>
<Route path="/:projectKey/dashboard" component={AsyncDashboard} />
<Route path="/:projectKey/products" component={AsyncProducts} />
{/* Define a catch-all route */}
<Route component={PageNotFound} />
</Switch>
)}
/>
</StoreProvider>
);
EntryPoint.displayName = 'EntryPoint';

ReactDOM.render(<EntryPoint />, document.getElementById('root'));
```
For an usage example, we recommend to look at the [application templates](https://github.com/commercetools/merchant-center-application-kit/tree/master/application-templates) examples or at the [Playground](https://github.com/commercetools/merchant-center-application-kit/tree/master/playground) application.

## Loading i18n messages with code splitting
### Loading i18n messages with code splitting

```js
// define a function that accepts a language, and returns a promise.
const loadApplicationMessagesForLanguage = lang =>
new Promise((resolve, reject) =>
import(`../../i18n/data/${lang}.json` /* webpackChunkName: "application-messages-[request]" */).then(
response => {
resolve(response.default);
},
error => {
reject(error);
}
)
import(`../../i18n/data/${lang}.json` /* webpackChunkName: "application-messages-[request]" */).then(
response => response.default
);

// pass this function to the <ApplicationShell />

const EntryPoint = () => (
<StoreProvider store={reduxStore}>
<ApplicationShell
applicationMessages={loadApplicationMessagesForLanguage}
// ... same as above.
/>
</StoreProvider>
<ApplicationShell
applicationMessages={loadApplicationMessagesForLanguage}
// ...other props
/>
);
```

## Props

| Props | Type | Required | Default | Description |
| -------------------------- | ------------------ | :------: | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `applicationMessages` | `object` or `func` | ✅ | - | Either an object containing all the translated messages per locale (`{ "en": { "Welcome": "Welcome" }, "de": { "Welcome": "Wilkommen" }}`), or a function that returns a promise that resolves to such an object. |
| `configuration` | `object` | ✅ | - | The current `window.app`. |
| `render` | `func` | ✅ | - | The function to render the application specific part. This function is executed only when the application specific part needs to be rendered. |
| `trackingEventWhitelist` | `object` | ✅ | - | An object containing a map of tracking events (_this mapping is required for backwards compatibility, it might be removed in the future_) |
| `onRegisterErrorListeners` | `func` | ✅ | - | A callback function to setup global event listeners, called when the `ApplicationShell` is mounted |
| Props | Type | Required | Default | Description |
| -------------------------------- | ------------------ | :---------------------: | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `applicationMessages` | `object` or `func` | ✅ | - | Either an object containing all the translated messages per locale (`{ "en": { "Welcome": "Welcome" }, "de": { "Welcome": "Wilkommen" }}`), or a function that returns a Promise that resolves to such an object. |
| `configuration` | `object` | ✅ | - | The current `window.app`. |
| `render` | `func` | ✅ | - | The function to render the application specific part. This function is executed only when the application specific part needs to be rendered. |
| `trackingEventWhitelist` | `object` | ✅ | - | An object containing a map of tracking events (_this mapping is required for backwards compatibility, it might be removed in the future_) |
| `onRegisterErrorListeners` | `func` | ✅ | - | A callback function to setup global event listeners, called when the `ApplicationShell` is mounted. The function is called with the following named arguments: `dispatch` (the dispatch function of Redux). |
| `DEV_ONLY__loadNavbarMenuConfig` | `func` | ✅ (`development` only) | - | A function that returns a Promise to load the `menu.json` config for the navigation component on the left side. We usually recommend to use a dynamic `import` to load the file, so that bundlers can create a split point. **NOTE that this is only available in `development` mode, in `production` mode the menu config is loaded from a remote server.** |
| `DEV_ONLY__loadAppbarMenuConfig` | `func` | ✅ (`development` only) | - | A function that returns a Promise to load the `menu.json` config for the account links in the application bar component on the top. We usually recommend to use a dynamic `import` to load the file, so that bundlers can create a split point. **NOTE that this is only available in `development` mode, in `production` mode the menu config is loaded from a remote server.** |

## Testing

Expand Down
12 changes: 11 additions & 1 deletion packages/application-shell/src/apollo-links/header-link.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ const isKnownTarget = target => Object.values(GRAPHQL_TARGETS).includes(target);
/* eslint-disable import/prefer-default-export */
// Use a middleware to update the request headers with the correct params.
const headerLink = new ApolloLink((operation, forward) => {
const { uri, cache } = operation.getContext();

// In case the `context` contains a `uri`, it means that we are not sending
// the request to the MC API, but to another server.
// For now, if that's the case, we skip the `target` validation and we do
// not send the custom headers.
if (uri) {
return forward(operation);
}

const target = operation.variables.target;
if (!isKnownTarget(target))
throw new Error(
Expand All @@ -27,7 +37,7 @@ const headerLink = new ApolloLink((operation, forward) => {
*/
const projectKey =
operation.variables.projectKey || selectProjectKeyFromUrl();
const userId = selectUserId({ apolloCache: operation.getContext().cache });
const userId = selectUserId({ apolloCache: cache });

// NOTE: keep header names with capital letters to avoid possible conflicts or problems with nginx.
operation.setContext({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ exports[`rendering should match layout structure 1`] = `
firstName="John"
gravatarHash="20c9c1b252b46ab49d6f7a4cee9c3e68"
lastName="Snow"
locale="en"
/>
</Inline>
</div>
Expand Down
6 changes: 6 additions & 0 deletions packages/application-shell/src/components/app-bar/app-bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,14 @@ const AppBar = props => {
<div className={styles.spacer} />
{props.user ? (
<UserSettingsMenu
locale={props.user.language}
firstName={props.user.firstName}
lastName={props.user.lastName}
gravatarHash={props.user.gravatarHash}
email={props.user.email}
DEV_ONLY__loadAppbarMenuConfig={
props.DEV_ONLY__loadAppbarMenuConfig
}
/>
) : (
<LoadingPlaceholder shape="dot" size="l" />
Expand All @@ -102,6 +106,7 @@ const AppBar = props => {
AppBar.displayName = 'AppBar';
AppBar.propTypes = {
user: PropTypes.shape({
language: PropTypes.string.isRequired,
gravatarHash: PropTypes.string.isRequired,
firstName: PropTypes.string.isRequired,
lastName: PropTypes.string.isRequired,
Expand All @@ -112,6 +117,7 @@ AppBar.propTypes = {
defaultProjectKey: PropTypes.string.isRequired,
}),
projectKeyFromUrl: PropTypes.string,
DEV_ONLY__loadAppbarMenuConfig: PropTypes.func,
};

export default AppBar;
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const createTestProps = props => ({
total: 0,
},
defaultProjectKey: 'test-default-project-key',
language: 'en',
firstName: 'John',
lastName: 'Snow',
email: 'john.snow@winter.com',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,9 @@ export const RestrictedApplication = props => (
<AppBar
user={user}
projectKeyFromUrl={projectKeyFromUrl}
DEV_ONLY__loadAppbarMenuConfig={
props.DEV_ONLY__loadAppbarMenuConfig
}
/>
</header>

Expand Down Expand Up @@ -248,6 +251,9 @@ export const RestrictedApplication = props => (
useFullRedirectsForLinks={
props.INTERNAL__isApplicationFallback
}
DEV_ONLY__loadNavbarMenuConfig={
props.DEV_ONLY__loadNavbarMenuConfig
}
/>
</ApplicationContextProvider>
);
Expand Down Expand Up @@ -355,6 +361,8 @@ RestrictedApplication.propTypes = {
applicationMessages: PropTypes.oneOfType([PropTypes.func, PropTypes.object])
.isRequired,
INTERNAL__isApplicationFallback: PropTypes.bool.isRequired,
DEV_ONLY__loadAppbarMenuConfig: PropTypes.func,
DEV_ONLY__loadNavbarMenuConfig: PropTypes.func,
};

/**
Expand Down Expand Up @@ -417,6 +425,9 @@ export default class ApplicationShell extends React.Component {
onRegisterErrorListeners: PropTypes.func.isRequired,
applicationMessages: PropTypes.oneOfType([PropTypes.func, PropTypes.object])
.isRequired,
// Only available in development mode
DEV_ONLY__loadAppbarMenuConfig: PropTypes.func,
DEV_ONLY__loadNavbarMenuConfig: PropTypes.func,
// Internal usage only, does not need to be documented
INTERNAL__isApplicationFallback: PropTypes.bool,
};
Expand Down Expand Up @@ -482,6 +493,12 @@ export default class ApplicationShell extends React.Component {
INTERNAL__isApplicationFallback={
this.props.INTERNAL__isApplicationFallback
}
DEV_ONLY__loadAppbarMenuConfig={
this.props.DEV_ONLY__loadAppbarMenuConfig
}
DEV_ONLY__loadNavbarMenuConfig={
this.props.DEV_ONLY__loadNavbarMenuConfig
}
/>
);
const browserLanguage = getBrowserLanguage(window);
Expand Down
Loading