[RFC] Enable decentralized routing and react hooks (#17)
This PR introduces a new project structure that helps maintain separation between the different experiences in baseline (e.g., Shell, Homepage) so people can start writing experiences without modifying the framework-level code. ## Folder Structure - `public/`: Contains the HTML file and public assets that need to exist outside the module system. See the section about [the public folder](https://facebook.github.io/create-react-app/docs/using-the-public-folder) for more information. - `src/`: You may create subdirectories inside src. For faster rebuilds, only files inside src are processed by Webpack. **You need to put any JS and CSS files inside `src`**, otherwise Webpack won’t see them. - `index.tsx`: This is the main javascript entry point. It initializes the base libraries (React, React Router, I18Next) and renders the `Shell`. - `shell/`: This folder contains all the components required to render the application's Shell (e.g., Masthead, Global Navigation, Workspace...). - `navigation.tsx`: Contains all the components that should be injected into the global navigation. - `routes.tsx`: Contains all the `Route`s that should be rendered in the shell `workspace`. - `areas/home/`, `areas/examples/`: These folders contain the application's own experiences. In general, an application will have multiple feature areas (e.g., Homepage, Settings) - probably mapping to the different entry points in the global navigation - that should be loaded as separate javascript bundles. The `examples` feature area, for instance, pulls in several large components to render lists and date-time pickers that are not necessary to render the `home` feature, so we should not fetch and load them until they're needed. ## Other changes - Update to React 16.8 to enable [Hooks](https://reactjs.org/docs/hooks-intro.html)! - Update to latest i18next to get the `useTranslation` hook and avoid creating higher-order components.
This commit is contained in:
Родитель
105e798faf
Коммит
3dae340e04
17
CHANGELOG.md
17
CHANGELOG.md
|
@ -1,5 +1,22 @@
|
|||
# CHANGELOG
|
||||
|
||||
## v4.0.0
|
||||
## Modified
|
||||
- Update to react@16.8 to get support for [Hooks](https://reactjs.org/docs/hooks-intro.html).
|
||||
- Update to i18next@14, which contains breaking API contract changes and supports react suspense and hooks
|
||||
- Update folder structure to align with team structure and make code contributions easier. Main changes:
|
||||
- Move all the bootstrapping code to `src/shell`.
|
||||
- Create `src/areas` to hold all feature code. `src/areas/home/home.tsx` is a good place to start.
|
||||
- Create `src/examples` to demonstrate how to use Routing and Fluent Controls.
|
||||
|
||||
### Migration Guide
|
||||
If you're migrating from baseline v2/v3:
|
||||
- Update your packages to match the new `package.json`.
|
||||
- Pull in `shell/`, `index.tsx`, `i18n.tsx`, and `errorBoundary.tsx` from `src/`.
|
||||
- Add your global navigation items in `shell/navigation.tsx`.
|
||||
- Add your routes in `shell/routes.tsx`.
|
||||
- `react-i18next` no longer provides a `TranslationFunction` or `I18n` HOC, so get it from `src/i18n.tsx` instead.
|
||||
|
||||
## v3.0.1
|
||||
### Modified
|
||||
- Update to latest fluent controls library to fix minor uialignment issues.
|
||||
|
|
23
README.md
23
README.md
|
@ -11,9 +11,9 @@ This project was bootstrapped with [Create React App](https://github.com/faceboo
|
|||
|
||||
## Getting Started
|
||||
|
||||
To get started with your own UX solution, fork this repo, run `npm install`, and start editing. `src/pages/App.tsx` is the main entry point and has examples of how all the above features work together.
|
||||
To get started with your own UX solution, fork this repo, run `npm install`, and start editing. `src/examples/index.tsx` is the one of the entry points and has examples of how all the above features work together.
|
||||
|
||||
You can learn more about the individual features [here](#learn-more).
|
||||
You can learn more about the folder structure [here](#folder-structure) and individual features [here](#learn-more).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
|
@ -52,6 +52,25 @@ Instead, it will copy all the configuration files and the transitive dependencie
|
|||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Folder Structure
|
||||
- `public/`:
|
||||
|
||||
Contains the HTML file and public assets that need to exist outside the module system. See the section about [the public folder](https://facebook.github.io/create-react-app/docs/using-the-public-folder) for more information.
|
||||
|
||||
- `src/`:
|
||||
|
||||
You may create subdirectories inside src. For faster rebuilds, only files inside src are processed by Webpack. **You need to put any JS and CSS files inside `src`**, otherwise Webpack won’t see them.
|
||||
|
||||
- `index.tsx`: This is the main javascript entry point. It initializes the base libraries (React, React Router, I18Next) and renders the `Shell`.
|
||||
|
||||
- `shell/`: This folder contains all the components required to render the application's Shell (e.g., Masthead, Global Navigation, Workspace...).
|
||||
|
||||
- `navigation.tsx`: Contains all the components that should be injected into the global navigation.
|
||||
|
||||
- `routes.tsx`: Contains all the `Route`s that should be rendered in the shell `workspace`.
|
||||
|
||||
- `areas/home/`, `areas/examples/`: These folders contain the application's own experiences. In general, an application will have multiple feature areas (e.g., Homepage, Settings) - probably mapping to the different entry points in the global navigation - that should be loaded as separate javascript bundles. The `examples` feature area, for instance, pulls in several large components to render lists and date-time pickers that are not necessary to render the `home` feature, so we should not fetch and load them until they're needed.
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
23
package.json
23
package.json
|
@ -1,32 +1,31 @@
|
|||
{
|
||||
"name": "@microsoft/azure-iot-ux-baseline",
|
||||
"version": "3.0.1",
|
||||
"version": "4.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@microsoft/azure-iot-ux-fluent-controls": "^6.0.3",
|
||||
"@microsoft/azure-iot-ux-fluent-css": "6.0.0",
|
||||
"classnames": "^2.2.6",
|
||||
"i18next": "^12.0.0",
|
||||
"i18next": "^14.0.1",
|
||||
"i18next-browser-languagedetector": "^2.2.4",
|
||||
"i18next-xhr-backend": "^1.5.1",
|
||||
"react": "^16.7.0",
|
||||
"react": "^16.8.1",
|
||||
"react-app-polyfill": "^0.2.0",
|
||||
"react-dom": "^16.7.0",
|
||||
"react-i18next": "^8.3.8",
|
||||
"react-dom": "^16.8.1",
|
||||
"react-i18next": "^10.0.0",
|
||||
"react-router-dom": "^4.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/i18next": "^11.9.3",
|
||||
"@types/i18next-browser-languagedetector": "^2.0.1",
|
||||
"@types/i18next-xhr-backend": "^1.4.1",
|
||||
"@types/jest": "^23.3.9",
|
||||
"@types/node": "^10.12.9",
|
||||
"@types/react": "^16.7.18",
|
||||
"@types/jest": "^23.3.13",
|
||||
"@types/node": "^10.12.21",
|
||||
"@types/react": "^16.8.1",
|
||||
"@types/react-dom": "^16.0.11",
|
||||
"@types/react-router-dom": "^4.3.1",
|
||||
"node-sass": "^4.10.0",
|
||||
"react-scripts": "^2.1.3",
|
||||
"typescript": "^3.1.6"
|
||||
"node-sass": "^4.11.0",
|
||||
"react-scripts": "^2.1.4",
|
||||
"typescript": "^3.3.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
|
|
|
@ -20,6 +20,29 @@
|
|||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
<!--
|
||||
Provide a default styling to show till the react app finishes loading. This has to work
|
||||
before any dependencies (CSS, JS) are available, so keep it self-contained and minimal:
|
||||
-->
|
||||
<style type="text/css">
|
||||
@keyframes empty-root-animation {
|
||||
0% { width: 0%; }
|
||||
5% { width: 0%; }
|
||||
100% { width: 90%; }
|
||||
}
|
||||
|
||||
#root:empty {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background-color: #60AAFF;
|
||||
animation-name: empty-root-animation;
|
||||
animation-duration: 10s;
|
||||
width: 90%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
@import '../../styles/colors';
|
||||
@import '../../styles/mixins';
|
||||
@import '../../styles/constants';
|
||||
|
||||
.container {
|
||||
margin: 0px $gutter-small;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import * as React from 'react';
|
||||
import { Link, Route, Switch } from 'react-router-dom';
|
||||
import classnames from 'classnames/bind';
|
||||
import { useTranslation } from '../../i18n';
|
||||
import { Paths } from '../../shell/routes';
|
||||
|
||||
import { List } from './list';
|
||||
import { Parameterized } from './parameterized';
|
||||
import { Inputs } from './inputs';
|
||||
|
||||
const cx = classnames.bind(require('./examples.module.scss'));
|
||||
|
||||
export default function Examples() {
|
||||
const [loc] = useTranslation();
|
||||
return (
|
||||
<div className={cx('container')}>
|
||||
<h1 className={cx('header')}>
|
||||
<Link to={Paths.examples.index} className='link'>{loc('navigation.examples')}</Link>
|
||||
</h1>
|
||||
<Switch>
|
||||
<Route path={Paths.examples.index} exact component={Root} />
|
||||
<Route path={Paths.examples.list} component={List} />
|
||||
<Route path={Paths.examples.inputs} component={Inputs} />
|
||||
<Route path={Paths.examples.parameterized} component={Parameterized} />
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Root() {
|
||||
return (
|
||||
<ul>
|
||||
<li><Link to={Paths.examples.list} className='link'>List</Link></li>
|
||||
<li><Link to={Paths.examples.inputs} className='link'>Inputs</Link></li>
|
||||
</ul>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
@import '../../styles/constants';
|
||||
|
||||
.container {
|
||||
max-width: $screen-sm;
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
import * as React from 'react';
|
||||
import * as classnames from 'classnames/bind';
|
||||
import { TextField, CheckboxField, ToggleField, RadioField, SelectField } from '@microsoft/azure-iot-ux-fluent-controls/lib/components/Field'
|
||||
import { DateTimeField } from '@microsoft/azure-iot-ux-fluent-controls/lib/components/DateTime'
|
||||
import { FormOption } from '@microsoft/azure-iot-ux-fluent-controls/lib/Common';
|
||||
|
||||
const cx = classnames.bind(require('./inputs.module.scss'));
|
||||
|
||||
export function Inputs() {
|
||||
// use hooks (https://reactjs.org/docs/hooks-intro.html) to maintain state:
|
||||
const [textValue, changeTextValue] = React.useState('');
|
||||
const [checkboxValue, changeCheckboxValue] = React.useState(true);
|
||||
const [toggleValue, changeToggleValue] = React.useState(true);
|
||||
const [radioValue, changeRadioValue] = React.useState('option1');
|
||||
const [dateTimeValue, changeDateTimeValue] = React.useState('');
|
||||
|
||||
return (
|
||||
<div className={cx('container')}>
|
||||
<h2>Inputs</h2>
|
||||
<TextField
|
||||
name='textField'
|
||||
value={textValue}
|
||||
onChange={changeTextValue}
|
||||
label='Text Field'
|
||||
tooltip='Description for a text field'
|
||||
required
|
||||
/>
|
||||
<CheckboxField
|
||||
name='checkboxField'
|
||||
value={checkboxValue}
|
||||
onChange={changeCheckboxValue}
|
||||
label='Checkbox Field'
|
||||
/>
|
||||
<ToggleField
|
||||
name='toggleField'
|
||||
value={toggleValue}
|
||||
onChange={changeToggleValue}
|
||||
label='Toggle Field'
|
||||
onLabel='Enabled'
|
||||
offLabel='Disabled'
|
||||
/>
|
||||
<RadioField
|
||||
name='radioField'
|
||||
value={radioValue}
|
||||
onChange={changeRadioValue}
|
||||
label='Radio Field'
|
||||
options={[
|
||||
{ value: 'option1', label: 'Option 1' },
|
||||
{ value: 'option2', label: 'Option 2' },
|
||||
]}
|
||||
/>
|
||||
<Select />
|
||||
<DateTimeField
|
||||
name='dateTimeField'
|
||||
initialValue={dateTimeValue}
|
||||
onChange={changeDateTimeValue}
|
||||
label='Date Time Field'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Select() {
|
||||
const [selectValue, changeSelectValue] = React.useState('');
|
||||
const [selectOptions, changeSelectOptions] = React.useState<FormOption[]>([
|
||||
// show a placeholder text initially:
|
||||
{ value: '', label: 'Loading…', disabled: true, hidden: true }
|
||||
]);
|
||||
|
||||
React.useEffect(() => {
|
||||
// load the actual options asynchronously. In practice, we'd probably call fetch()
|
||||
// to make an HTTP call and call changeSelectOptions() after the promise resolves.
|
||||
const handle = setTimeout(() => {
|
||||
changeSelectOptions([
|
||||
// Replace the placeholder text now that we've finished loading:
|
||||
{ value: '', label: 'Select an option', hidden: true, disabled: true },
|
||||
|
||||
// actual options:
|
||||
{ value: 'option1', label: 'Option 1' },
|
||||
{ value: 'option2', label: 'Option 2' },
|
||||
]);
|
||||
}, 2000);
|
||||
|
||||
// return a function that cleans up after this effect (e.g., if the
|
||||
// component unloads before the options are fetched):
|
||||
return () => clearTimeout(handle);
|
||||
}, [changeSelectOptions]);
|
||||
|
||||
return (
|
||||
<SelectField
|
||||
name='selectField'
|
||||
value={selectValue}
|
||||
onChange={changeSelectValue}
|
||||
label='Select Field'
|
||||
options={selectOptions}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
import * as React from 'react';
|
||||
import { GenericManagementList } from '@microsoft/azure-iot-ux-fluent-controls/lib/components/List';
|
||||
import { Link, generatePath } from 'react-router-dom';
|
||||
import { Paths } from '../../shell/routes';
|
||||
|
||||
export interface Properties {
|
||||
}
|
||||
|
||||
interface Row {
|
||||
id: string;
|
||||
name: string;
|
||||
location: string;
|
||||
}
|
||||
|
||||
export function List() {
|
||||
const [rows] = React.useState(() => [
|
||||
{ id: 'foo', name: 'Foo', location: 'Seattle' },
|
||||
{ id: 'bar', name: 'Bar', location: 'Redmond' },
|
||||
]);
|
||||
|
||||
const [selected, changeSelected] = React.useState(new Set<string>());
|
||||
function isSelected(row: Row) {
|
||||
return selected.has(row.id);
|
||||
}
|
||||
|
||||
function onSelect(row: Row) {
|
||||
const newSelection = new Set<string>(selected);
|
||||
if (!newSelection.delete(row.id)) {
|
||||
newSelection.add(row.id);
|
||||
}
|
||||
|
||||
changeSelected(newSelection);
|
||||
}
|
||||
|
||||
function onSelectAll() {
|
||||
const newSelected = new Set<string>();
|
||||
if (rows.length !== selected.size) {
|
||||
for (const row of rows) {
|
||||
newSelected.add(row.id);
|
||||
}
|
||||
}
|
||||
|
||||
changeSelected(newSelected);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>List</h2>
|
||||
<GenericManagementList<Row>
|
||||
rows={rows}
|
||||
columns={[
|
||||
{ label: 'Name', mapColumn: mapNameCol },
|
||||
{ label: 'Location', mapColumn: 'location' },
|
||||
]}
|
||||
isSelected={isSelected}
|
||||
onSelect={onSelect}
|
||||
onSelectAll={onSelectAll}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function mapNameCol(row: Row) {
|
||||
return (
|
||||
<Link to={generatePath(Paths.examples.parameterized, { id: row.id })} className='link'>{row.name}</Link>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import * as React from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
export function Parameterized({ match }: RouteComponentProps<{ id: string }>) {
|
||||
return (
|
||||
<h4>This is a parameterized route for: {match.params.id}</h4>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
.header {
|
||||
text-align: center;
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import * as React from 'react';
|
||||
import classnames from 'classnames/bind';
|
||||
import { useTranslation } from '../../i18n';
|
||||
const cx = classnames.bind(require('./home.module.scss'));
|
||||
|
||||
export default function Home() {
|
||||
const [loc] = useTranslation();
|
||||
return (
|
||||
<h1 className={cx('header')}>{loc('navigation.home')}</h1>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import React from 'react';
|
||||
|
||||
interface Properties {
|
||||
message: React.ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
}
|
||||
|
||||
export class ErrorBoundary extends React.Component<Properties, State> {
|
||||
constructor(props: Properties) {
|
||||
super(props);
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error) {
|
||||
// Update state so the next render will show the fallback UI.
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
||||
// You can also log the error to an error reporting service
|
||||
// logErrorToMyService(error, info);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
// You can render any custom fallback UI
|
||||
return <h2>{this.props.message}</h2>;
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
64
src/i18n.tsx
64
src/i18n.tsx
|
@ -1,31 +1,40 @@
|
|||
import * as React from 'react';
|
||||
import * as i18next from 'i18next';
|
||||
import i18next from 'i18next';
|
||||
import Backend from 'i18next-xhr-backend';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
import { NamespacesConsumer, ReactI18NextOptions, Namespace } from 'react-i18next';
|
||||
import { initReactI18next, useTranslation } from 'react-i18next';
|
||||
|
||||
// for convenience and backwards compatibility, re-export NamespacesConsumer
|
||||
export const I18n: React.ComponentClass<I18nProperties> = NamespacesConsumer as any;
|
||||
export interface I18nProperties extends ReactI18NextOptions {
|
||||
ns?: Namespace;
|
||||
initialI18nStore?: {};
|
||||
initialLanguage?: string;
|
||||
/** The i18next translate function, exposed here as `TranslationFunction` for backwards compatibility. */
|
||||
export type TranslationFunction = i18next.TFunction;
|
||||
|
||||
/** Re-export the translation hook and Trans component here for convenience. */
|
||||
export { useTranslation, Trans } from 'react-i18next';
|
||||
|
||||
/**
|
||||
* Component that provides the translation function as a render prop, for backwards compatibility.
|
||||
* @deprecated use the `useTranslation` hook instead.
|
||||
*/
|
||||
export function I18n({ children }: {
|
||||
children(
|
||||
t: i18next.TranslationFunction,
|
||||
options: {
|
||||
i18n: i18next.i18n;
|
||||
lng: string;
|
||||
ready: boolean;
|
||||
}
|
||||
): React.ReactNode;
|
||||
t: i18next.TFunction,
|
||||
i18n: i18next.i18n
|
||||
): JSX.Element;
|
||||
}) {
|
||||
const [t, i18n] = useTranslation();
|
||||
return children(t, i18n);
|
||||
}
|
||||
|
||||
let instance: i18next.i18n;
|
||||
export function getInstance() {
|
||||
if (instance) {
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the `i18next` instance, provided here only for completeness. In most
|
||||
* cases, the `i18n` instance should be fetched from the `useTranslation` hook
|
||||
* so the component re-renders when the language changes.
|
||||
*/
|
||||
export function getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
export function initInstance() {
|
||||
// call require() on each resjson in the localesDir so it is added to webpack's
|
||||
// dependency graph. (require on a resjson just returns the output file path)
|
||||
const context = (require as any).context('./locales/', true, /\.resjson$/);
|
||||
|
@ -42,7 +51,8 @@ export function getInstance() {
|
|||
|
||||
instance = i18next
|
||||
.use(Backend)
|
||||
.use(LanguageDetector);
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next);
|
||||
|
||||
const postProcessors: string[] = [];
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
|
@ -70,7 +80,7 @@ export function getInstance() {
|
|||
case 'rtl':
|
||||
// change language to hebrew to force RTL direction, and return a pseudo string:
|
||||
instance.changeLanguage(rtlLocaleName);
|
||||
// fall through to he-PSEUDO
|
||||
// fall through to he-PSEUDO
|
||||
case rtlLocaleName:
|
||||
return 'ץהמלצצהדםןףזזצנןזבםוקדםכפנחזרגודמרהרעהייסנשחןיהתכןוקןןוטלפפלתנגהקיץףפנתשחףכילדןצזדשתשקחןנקרםףץהעבצימםזפמתשברחעלתץנתמנרחנסהטסמךזהרךסנםםדהבוכסאדדנגצכפולחאץעתמםצץהןשךקיאץץצץלףנשנחיכתךסירשיסגאלספמגבןזףםךפשעתהלםץגץאדטגצבפחערשסעצץכץחפסוםלבקוםץפנטדהלףאןלקנףןבהרדצזדלדתקסיטחדןצגובףסעמנרזאתץקיםיקעדןבעןגץשתימפזסזגףיעםבבדפמןנםמנםפנרלסגבכדתספצתעגובכוטרםךליםףתהץצפסצןזהץצמוזמרטלסםקזירבבזדולרףחטהעחתתקהףתךםקקהךץםדירתצפגגבךכחזכבןיליגתמשחמחלימדטישהמןלשהצלפזהףפידעאףנןכתךץתףןסנאךחתץדטמבצךוקחףפםמחימטזסנמכחתצרלשעכשגבגפיהתךךקגפבצעפשהאןצגטלחנדאץתפקךדזםסהינכיטזחשדאותץםחזהדתםגןלךדשילםאסעקגגףדששתסצףוץבןקהיקצתקצודאףףסנלפיאממבשףמבנגךבםןתמךסץהעךגקוךסצמצדךאףףרכסשאתחזקהזכףלןםטשןלאשמגלרוסזץהםויחחעגפפבףסתתורהףהבזצךברנסיצוצקנבךשץךתצךדגדשזלזבבצצדשרנרץאץאתפםטףהדיכתשששטחוצתדיףרקשכץפסבחשףןעזקמסנפףהזרנושמפשץדגנכזסחודגועאדזקךשךדההיפחסכינץבעערמתצגץףעךשלטקיבודןמחאגבירפהאץלץכךעחולעדםאכזאגכשדךחףךפףהדזטטכיזץכטקכסזץתהעצקקחצךטושאןץשזץריףםתםתגיזדזנןקמישדץםדגדמצסקבמטצןדטיכצרךדכטתףטשטחשנמכךהסצאצמקותטשכאבכיסחיוקסןאקבשסךףזסהןרדהייהעףםבטצרעקךהךצטםרנךיטרחגיגמדםאכך';
|
||||
default:
|
||||
|
@ -80,6 +90,11 @@ export function getInstance() {
|
|||
});
|
||||
}
|
||||
|
||||
instance.on('languageChanged', lang => {
|
||||
// update the html lang attribute:
|
||||
document.documentElement.lang = lang;
|
||||
});
|
||||
|
||||
instance.init({
|
||||
// backend options: https://github.com/i18next/i18next-xhr-backend#backend-options
|
||||
backend: {
|
||||
|
@ -114,10 +129,5 @@ export function getInstance() {
|
|||
},
|
||||
|
||||
postProcess: postProcessors,
|
||||
react: {
|
||||
wait: true
|
||||
}
|
||||
});
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
|
|
@ -4,17 +4,33 @@ import 'react-app-polyfill/ie11';
|
|||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { getInstance as getI18nInstance } from './i18n';
|
||||
import App from './pages/App';
|
||||
import * as i18n from './i18n';
|
||||
import { ErrorBoundary } from './errorBoundary';
|
||||
import Shell from './shell/shell';
|
||||
import * as serviceWorker from './serviceWorker';
|
||||
|
||||
// initialize the i18next instance before rendering anything:
|
||||
i18n.initInstance();
|
||||
|
||||
// Render <Shell> in the #root dom element. Shell requires:
|
||||
// 1. <BrowserRouter> that provides the routing functionality.
|
||||
// 2. <Suspense> that provides a fallback when we're async-loading the js bundles
|
||||
// and i18n resjsons. The fallback should result in an empty #root element,
|
||||
// which triggers the default loading styles in public/index.html.
|
||||
// 3. <ErrorBoundary> that displays an error message if something fails with the
|
||||
// async request. At this point, we can't rely on anything else working so
|
||||
// just display a static hardcoded message.
|
||||
// 4. <StrictMode> to highlight problems with the application code.
|
||||
ReactDOM.render(
|
||||
<I18nextProvider i18n={getI18nInstance()}>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</I18nextProvider>,
|
||||
<React.StrictMode>
|
||||
<ErrorBoundary message='Something went wrong'>
|
||||
<React.Suspense fallback=''>
|
||||
<BrowserRouter>
|
||||
<Shell />
|
||||
</BrowserRouter>
|
||||
</React.Suspense>
|
||||
</ErrorBoundary>
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root'));
|
||||
|
||||
// If you want your app to work offline and load faster, you can change
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
{
|
||||
"masthead": "Azure IoT Grundlinie Benutzererfahrung",
|
||||
"navigation": {
|
||||
"collapse": "Seitennavigationsleiste reduzieren",
|
||||
"expand": "Seitennavigationsleiste erweitern",
|
||||
"home": "Zuhause",
|
||||
"about": "Über"
|
||||
"examples": "Beispiele"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
{
|
||||
"masthead": "Azure IoT UX Baseline",
|
||||
"navigation": {
|
||||
"collapse": "Collapse Side Navigation",
|
||||
"expand": "Expand Side Navigation",
|
||||
"home": "Homepage",
|
||||
"about": "About"
|
||||
"examples": "Examples"
|
||||
},
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"logout": "Logout",
|
||||
"more": "More",
|
||||
"errors": {
|
||||
"default": "Something went wrong"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"theme": "Theme",
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
@import '../styles/colors';
|
||||
@import '../styles/mixins';
|
||||
@import '../styles/constants';
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
}
|
|
@ -1,216 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { TranslationFunction } from 'i18next';
|
||||
import { Route, Switch, NavLink } from 'react-router-dom';
|
||||
import classnames from 'classnames/bind';
|
||||
import { Shell, NavigationProperties, MastheadProperties } from '@microsoft/azure-iot-ux-fluent-controls/lib/components/Shell';
|
||||
|
||||
import { I18n } from '../i18n';
|
||||
import { Settings, SettingsPanel, Themes } from './Settings';
|
||||
import { HelpPanel } from './Help';
|
||||
|
||||
import './App.fonts.scss';
|
||||
import { Button } from '@microsoft/azure-iot-ux-fluent-controls/lib/components/Button';
|
||||
const cx = classnames.bind(require('./App.module.scss'));
|
||||
|
||||
interface Properties {
|
||||
}
|
||||
|
||||
interface State {
|
||||
expanded?: 'userMenu' | 'settingsPanel' | 'helpPanel' | 'moreMenu' | 'navMenu' | null;
|
||||
isNavExpanded: boolean;
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
export class App extends React.Component<Properties, State> {
|
||||
constructor(props: Properties) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isNavExpanded: true,
|
||||
settings: {
|
||||
theme: Themes.light,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { expanded, settings } = this.state;
|
||||
return (
|
||||
<I18n>{(loc, { i18n }) => {
|
||||
const navProps = this.getNav(loc);
|
||||
const mastheadProps = this.getMasthead(loc, navProps);
|
||||
return (
|
||||
<Shell
|
||||
theme={settings.theme}
|
||||
isRtl={i18n.dir() === 'rtl'}
|
||||
navigation={navProps}
|
||||
masthead={mastheadProps}
|
||||
onClick={this.handleViewCollapse}>
|
||||
<Switch>
|
||||
<Route exact path='/' component={Home} />
|
||||
<Route path='/about' component={About} />
|
||||
</Switch>
|
||||
<div onClick={this.blockViewCollapse}>
|
||||
{expanded === 'settingsPanel' && <SettingsPanel settings={settings} onSave={this.handleSettingsSave} onCancel={this.handleViewCollapse} loc={loc} />}
|
||||
{expanded === 'helpPanel' && <HelpPanel onCancel={this.handleViewCollapse} loc={loc} />}
|
||||
</div>
|
||||
</Shell>
|
||||
);
|
||||
}}</I18n>
|
||||
);
|
||||
}
|
||||
|
||||
getNav(loc: TranslationFunction): NavigationProperties {
|
||||
const items = [
|
||||
{
|
||||
key: 'home',
|
||||
to: '/',
|
||||
exact: true,
|
||||
icon: 'icon icon-home',
|
||||
label: loc('navigation.home'),
|
||||
title: loc('navigation.home')
|
||||
},
|
||||
{
|
||||
key: 'about',
|
||||
to: '/about',
|
||||
icon: 'icon icon-multitask',
|
||||
label: loc('navigation.about'),
|
||||
title: loc('navigation.about')
|
||||
}
|
||||
];
|
||||
|
||||
return {
|
||||
isExpanded: this.state.isNavExpanded,
|
||||
onClick: this.handleGlobalNavToggle,
|
||||
attr: {
|
||||
navButton: {
|
||||
title: this.state.isNavExpanded ? 'Collapse side navigation' : 'Expand side navigation',
|
||||
},
|
||||
},
|
||||
children: items.map(x => (
|
||||
<NavLink to={x.to} exact={x.exact} key={x.key} title={x.title} className='global-nav-item' activeClassName='global-nav-item-active'>
|
||||
<span className={cx('global-nav-item-icon', x.icon)} />
|
||||
<span className={cx('inline-text-overflow', 'global-nav-item-text')}>{x.label}</span>
|
||||
</NavLink>
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
getUserMenuItems(loc: TranslationFunction) {
|
||||
const title = loc('logout');
|
||||
return (
|
||||
<Button onClick={this.handleLogout} title={title}>
|
||||
{title}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
getMasthead(loc: TranslationFunction, navProps: NavigationProperties): MastheadProperties {
|
||||
const { expanded } = this.state;
|
||||
return {
|
||||
branding: loc('masthead'),
|
||||
more: {
|
||||
onClick: this.handleClickMore,
|
||||
selected: expanded === 'moreMenu',
|
||||
title: loc('more')
|
||||
},
|
||||
navigation: {
|
||||
onClick: this.handleClickNavMenu,
|
||||
isExpanded: expanded === 'navMenu',
|
||||
attr: navProps.attr,
|
||||
children: navProps.children,
|
||||
},
|
||||
toolbarItems: [
|
||||
{ icon: 'settings', label: loc('settings.title'), onClick: this.handleContextPanelOpenSettings, selected: expanded === "settingsPanel", attr: { button: { 'aria-label': loc('settings.title') } } },
|
||||
{ icon: 'help', label: loc('help.title'), onClick: this.handleContextPanelOpenHelp, selected: expanded === 'helpPanel', attr: { button: { 'aria-label': loc('help.title') } } },
|
||||
],
|
||||
user: {
|
||||
onMenuClick: this.handleClickUserIcon,
|
||||
menuExpanded: expanded === 'userMenu',
|
||||
menuItems: this.getUserMenuItems(loc),
|
||||
displayName: 'John P',
|
||||
email: 'johnp@contoso.com'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
handleContextPanelOpenHelp = (e?: React.MouseEvent<any>) => {
|
||||
e && e.stopPropagation();
|
||||
this.setState({
|
||||
expanded: this.state.expanded !== 'helpPanel' ? 'helpPanel' : null,
|
||||
});
|
||||
}
|
||||
|
||||
handleContextPanelOpenSettings = (e?: React.MouseEvent<any>) => {
|
||||
e && e.stopPropagation();
|
||||
this.setState({
|
||||
expanded: this.state.expanded !== 'settingsPanel' ? 'settingsPanel' : null,
|
||||
});
|
||||
}
|
||||
|
||||
handleSettingsSave = (newSettings: Settings) => {
|
||||
this.setState({
|
||||
settings: newSettings,
|
||||
expanded: null,
|
||||
});
|
||||
}
|
||||
|
||||
handleLogout = (e: React.MouseEvent<any>) => {
|
||||
e && e.stopPropagation();
|
||||
alert('logout');
|
||||
}
|
||||
|
||||
|
||||
handleClickUserIcon = (e: React.MouseEvent<any>) => {
|
||||
e && e.stopPropagation();
|
||||
this.setState({
|
||||
expanded: this.state.expanded !== 'userMenu' ? 'userMenu' : null,
|
||||
});
|
||||
}
|
||||
|
||||
handleGlobalNavToggle = (e: React.MouseEvent<any>) => {
|
||||
e && e.stopPropagation();
|
||||
this.setState({
|
||||
isNavExpanded: !this.state.isNavExpanded
|
||||
});
|
||||
}
|
||||
|
||||
handleClickMore = (e: React.MouseEvent<any>) => {
|
||||
e && e.stopPropagation();
|
||||
this.setState({
|
||||
expanded: this.state.expanded !== 'moreMenu' ? 'moreMenu' : null,
|
||||
});
|
||||
}
|
||||
|
||||
handleClickNavMenu = (e?: React.MouseEvent<any>) => {
|
||||
e && e.stopPropagation();
|
||||
this.setState({
|
||||
expanded: this.state.expanded !== 'navMenu' ? 'navMenu' : null,
|
||||
});
|
||||
}
|
||||
|
||||
handleViewCollapse = (e?: React.MouseEvent<any>) => {
|
||||
e && e.stopPropagation();
|
||||
this.setState({
|
||||
expanded: null,
|
||||
});
|
||||
}
|
||||
|
||||
blockViewCollapse = (e?: React.MouseEvent<any>) => {
|
||||
e && e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
|
||||
const Home = () => (
|
||||
<I18n>{loc =>
|
||||
<h1 className={cx('header')}>{loc('navigation.home')}</h1>
|
||||
}</I18n>
|
||||
);
|
||||
|
||||
const About = () => (
|
||||
<I18n>{loc =>
|
||||
<h1 className={cx('header')}>{loc('navigation.about')}</h1>
|
||||
}</I18n>
|
||||
);
|
|
@ -1,76 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { ContextPanel } from '@microsoft/azure-iot-ux-fluent-controls/lib/components/ContextPanel';
|
||||
import { Button } from '@microsoft/azure-iot-ux-fluent-controls/lib/components/Button';
|
||||
import { SelectField } from '@microsoft/azure-iot-ux-fluent-controls/lib/components/Field/SelectField';
|
||||
import { TranslationFunction } from 'i18next';
|
||||
|
||||
export interface Settings {
|
||||
theme: string;
|
||||
}
|
||||
|
||||
export interface Properties {
|
||||
loc: TranslationFunction;
|
||||
settings: Settings;
|
||||
onSave: (newSettings: Settings) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const Themes = {
|
||||
dark: 'dark',
|
||||
light: 'light',
|
||||
}
|
||||
|
||||
export class SettingsPanel extends React.Component<Properties, Settings> {
|
||||
constructor(props: Properties) {
|
||||
super(props);
|
||||
// copy the settings over to state so we can change it before hitting save:
|
||||
this.state = {
|
||||
theme: props.settings.theme,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loc, onCancel } = this.props;
|
||||
return (
|
||||
<ContextPanel
|
||||
header={loc('settings.title')}
|
||||
footer={this.renderFooter()}
|
||||
onClose={onCancel}
|
||||
>
|
||||
<SelectField
|
||||
name='theme'
|
||||
label={loc('settings.theme')}
|
||||
value={this.state.theme}
|
||||
options={[
|
||||
{ label: loc('settings.themes.dark'), value: Themes.dark },
|
||||
{ label: loc('settings.themes.light'), value: Themes.light }
|
||||
]}
|
||||
autoFocus
|
||||
onChange={this.handleThemeChange}
|
||||
/>
|
||||
</ContextPanel>
|
||||
);
|
||||
}
|
||||
|
||||
renderFooter() {
|
||||
const { loc, onCancel } = this.props;
|
||||
return (
|
||||
<>
|
||||
<Button onClick={this.handleSave} primary>{loc('save')}</Button>
|
||||
<Button onClick={onCancel}>{loc('cancel')}</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
private handleThemeChange = (newValue: string) => {
|
||||
this.setState({
|
||||
theme: newValue
|
||||
});
|
||||
}
|
||||
|
||||
private handleSave = () => {
|
||||
this.props.onSave({
|
||||
theme: this.state.theme
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from 'react';
|
||||
import { ContextPanel } from '@microsoft/azure-iot-ux-fluent-controls/lib/components/ContextPanel';
|
||||
import { TranslationFunction } from 'i18next';
|
||||
import { TranslationFunction } from '../i18n';
|
||||
|
||||
export interface Properties {
|
||||
loc: TranslationFunction;
|
||||
|
@ -9,10 +9,7 @@ export interface Properties {
|
|||
|
||||
export function HelpPanel({ loc, onCancel }: Properties) {
|
||||
return (
|
||||
<ContextPanel
|
||||
header={loc('help.title')}
|
||||
onClose={onCancel}
|
||||
>
|
||||
<ContextPanel header={loc('help.title')} onClose={onCancel}>
|
||||
<a href="https://github.com/Azure/iot-ux-baseline" target="_blank">{loc('help.getStarted')}</a>
|
||||
</ContextPanel>
|
||||
);
|
|
@ -0,0 +1,33 @@
|
|||
import * as React from 'react';
|
||||
import classnames from 'classnames/bind';
|
||||
import { TranslationFunction } from '../i18n';
|
||||
|
||||
import { Paths } from './routes';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
const cx = classnames.bind(null);
|
||||
|
||||
/** Declares all the items that need to be injected into the global navigation. */
|
||||
export function Navigation({ loc }: { loc: TranslationFunction}) {
|
||||
return (
|
||||
<>
|
||||
<NavItem to={Paths.home.index} exact title={loc('navigation.home')} icon='icon-home' text={loc('navigation.home')} />
|
||||
<NavItem to={Paths.examples.index} title={loc('navigation.examples')} icon='icon-education' text={loc('navigation.examples')} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function NavItem({ to, exact, title, icon, text }: {
|
||||
to: string;
|
||||
exact?: boolean;
|
||||
title: string;
|
||||
icon: string;
|
||||
text: string;
|
||||
}) {
|
||||
return (
|
||||
<NavLink to={to} exact={exact} title={title} className='global-nav-item' activeClassName='global-nav-item-active'>
|
||||
<span className={cx('global-nav-item-icon', 'icon', icon)} />
|
||||
<span className={cx('inline-text-overflow', 'global-nav-item-text')}>{text}</span>
|
||||
</NavLink>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import * as React from 'react';
|
||||
import { Switch, Route } from 'react-router-dom';
|
||||
|
||||
// Load the container components of each area lazily in its own bundle.
|
||||
// In general, each area should be isolated to its own bundle.
|
||||
const Home = React.lazy(() => import('../areas/home/home'));
|
||||
const Examples = React.lazy(() => import('../areas/examples/examples'));
|
||||
|
||||
/**
|
||||
* Declares all the route paths in this app so we can deep link anywhere
|
||||
* without having to construct route path strings manually:
|
||||
*/
|
||||
export const Paths = {
|
||||
home: {
|
||||
index: '/',
|
||||
},
|
||||
examples: {
|
||||
index: '/examples',
|
||||
parameterized: '/examples/parameterizedRoutes/:id',
|
||||
list: '/examples/list',
|
||||
inputs: '/examples/inputs',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Declares all routes that need to be rendered in the main shell workspace. In most
|
||||
* cases, we just need one container component per area that should be loaded lazily:
|
||||
* all descendant routes should be declared within the container.
|
||||
*/
|
||||
export function Routes() {
|
||||
return (
|
||||
<Switch>
|
||||
<Route exact path={Paths.home.index} component={Home} />
|
||||
<Route path={Paths.examples.index} component={Examples} />
|
||||
</Switch>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import * as React from 'react';
|
||||
import { ContextPanel } from '@microsoft/azure-iot-ux-fluent-controls/lib/components/ContextPanel';
|
||||
import { Button } from '@microsoft/azure-iot-ux-fluent-controls/lib/components/Button';
|
||||
import { SelectField } from '@microsoft/azure-iot-ux-fluent-controls/lib/components/Field/SelectField';
|
||||
import { TranslationFunction } from '../i18n';
|
||||
|
||||
export interface Settings {
|
||||
theme: string;
|
||||
}
|
||||
|
||||
export interface Properties {
|
||||
loc: TranslationFunction;
|
||||
settings: Settings;
|
||||
onSave: (newSettings: Settings) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const Themes = {
|
||||
dark: 'dark',
|
||||
light: 'light',
|
||||
}
|
||||
|
||||
export function SettingsPanel({ loc, onSave, onCancel, settings }: Properties) {
|
||||
const [theme, changeTheme] = React.useState(settings.theme);
|
||||
return (
|
||||
<ContextPanel
|
||||
header={loc('settings.title')}
|
||||
footer={
|
||||
<>
|
||||
<Button onClick={() => onSave({ theme })} primary>{loc('save')}</Button>
|
||||
<Button onClick={onCancel}>{loc('cancel')}</Button>
|
||||
</>
|
||||
}
|
||||
onClose={onCancel}
|
||||
>
|
||||
<SelectField
|
||||
name='theme'
|
||||
label={loc('settings.theme')}
|
||||
value={theme}
|
||||
options={[
|
||||
{ label: loc('settings.themes.dark'), value: Themes.dark },
|
||||
{ label: loc('settings.themes.light'), value: Themes.light }
|
||||
]}
|
||||
autoFocus
|
||||
onChange={changeTheme}
|
||||
/>
|
||||
</ContextPanel>
|
||||
);
|
||||
}
|
|
@ -1,27 +1,25 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import i18next from 'i18next';
|
||||
import App from './App';
|
||||
import Shell from './shell';
|
||||
|
||||
it('renders without crashing', () => {
|
||||
const div = document.createElement('div');
|
||||
const i18n = i18next
|
||||
i18next
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
ns: ['translations'],
|
||||
defaultNS: 'translations',
|
||||
interpolation: {
|
||||
escapeValue: false, // not needed for react!!
|
||||
},
|
||||
react: {
|
||||
wait: true
|
||||
}
|
||||
});
|
||||
|
||||
ReactDOM.render(
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<App />
|
||||
</I18nextProvider>,
|
||||
<React.Suspense fallback=''>
|
||||
<Shell />
|
||||
</React.Suspense>,
|
||||
div);
|
||||
|
||||
ReactDOM.unmountComponentAtNode(div);
|
|
@ -0,0 +1,124 @@
|
|||
import * as React from 'react';
|
||||
import { Button } from '@microsoft/azure-iot-ux-fluent-controls/lib/components/Button';
|
||||
import { Shell as FluentShell, NavigationProperties, MastheadProperties } from '@microsoft/azure-iot-ux-fluent-controls/lib/components/Shell';
|
||||
import { HorizontalLoader } from '@microsoft/azure-iot-ux-fluent-controls/lib/components/Loader/HorizontalLoader';
|
||||
|
||||
import { TranslationFunction, useTranslation } from '../i18n';
|
||||
import { Settings, SettingsPanel, Themes } from './settings';
|
||||
import { HelpPanel } from './help';
|
||||
import { Routes } from './routes';
|
||||
import { Navigation } from './navigation';
|
||||
|
||||
import './shell.fonts.scss';
|
||||
import { ErrorBoundary } from '../errorBoundary';
|
||||
|
||||
export default function Shell() {
|
||||
const [loc, i18n] = useTranslation();
|
||||
const [expanded, changeExpanded] = React.useState<string>('');
|
||||
const [settings, changeSettings] = React.useState({ theme: Themes.light });
|
||||
function handleViewCollapse() {
|
||||
// reset expanded to its default state. IMPORTANT: don't stop event
|
||||
// propagation here: this will block clicking behavior for some html
|
||||
// elements like <a>, input checkboxes, and buttons.
|
||||
changeExpanded('');
|
||||
}
|
||||
|
||||
function handleSettingsSave(newSettings: Settings) {
|
||||
changeSettings(newSettings);
|
||||
handleViewCollapse();
|
||||
}
|
||||
|
||||
const navProps = useNavigationProperties(loc);
|
||||
const mastheadProps = getMastheadProperties(loc, expanded, changeExpanded, navProps)
|
||||
return (
|
||||
<FluentShell
|
||||
theme={settings.theme}
|
||||
isRtl={i18n.dir() === 'rtl'}
|
||||
navigation={navProps}
|
||||
masthead={mastheadProps}
|
||||
onClick={handleViewCollapse}>
|
||||
<ErrorBoundary message={loc('errors.default')}>
|
||||
<React.Suspense fallback={<HorizontalLoader />}>
|
||||
<Routes />
|
||||
</React.Suspense>
|
||||
</ErrorBoundary>
|
||||
<div onClick={blockViewCollapse}>
|
||||
{expanded === 'settingsPanel' && <SettingsPanel settings={settings} onSave={handleSettingsSave} onCancel={handleViewCollapse} loc={loc} />}
|
||||
{expanded === 'helpPanel' && <HelpPanel onCancel={handleViewCollapse} loc={loc} />}
|
||||
</div>
|
||||
</FluentShell>
|
||||
);
|
||||
}
|
||||
|
||||
function useNavigationProperties(loc: TranslationFunction): NavigationProperties {
|
||||
const [isExpanded, changeExpanded] = React.useState(true);
|
||||
return {
|
||||
isExpanded: isExpanded,
|
||||
onClick: () => {
|
||||
// toggle expanded and let the event propagate up to collapse any expanded views:
|
||||
changeExpanded(!isExpanded);
|
||||
},
|
||||
attr: {
|
||||
navButton: {
|
||||
title: loc(isExpanded ? 'navigation.collapse': 'navigation.expand'),
|
||||
},
|
||||
},
|
||||
children: <Navigation loc={loc} />
|
||||
}
|
||||
}
|
||||
|
||||
function getMastheadProperties(loc: TranslationFunction, expanded: string, changeExpanded: (expanded: string) => void, navProps: NavigationProperties): MastheadProperties {
|
||||
return {
|
||||
branding: loc('masthead'),
|
||||
more: {
|
||||
onClick: getExpandCallback('moreMenu', changeExpanded),
|
||||
selected: expanded === 'moreMenu',
|
||||
title: loc('more')
|
||||
},
|
||||
navigation: {
|
||||
isExpanded: expanded === 'navMenu',
|
||||
onClick: getExpandCallback('navMenu', changeExpanded),
|
||||
attr: navProps.attr,
|
||||
children: navProps.children
|
||||
},
|
||||
toolbarItems: [
|
||||
{
|
||||
icon: 'settings',
|
||||
label: loc('settings.title'),
|
||||
onClick: getExpandCallback('settingsPanel', changeExpanded),
|
||||
selected: expanded === "settingsPanel",
|
||||
attr: { button: { 'aria-label': loc('settings.title') } }
|
||||
},
|
||||
{
|
||||
icon: 'help',
|
||||
label: loc('help.title'),
|
||||
onClick: getExpandCallback('helpPanel', changeExpanded),
|
||||
selected: expanded === 'helpPanel',
|
||||
attr: { button: { 'aria-label': loc('help.title') } }
|
||||
},
|
||||
],
|
||||
user: {
|
||||
onMenuClick: getExpandCallback('userMenu', changeExpanded),
|
||||
menuExpanded: expanded === 'userMenu',
|
||||
menuItems: (<Button onClick={logout} title={loc('logout')}>{loc('logout')}</Button>),
|
||||
displayName: 'John P',
|
||||
email: 'johnp@contoso.com'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function logout(e: React.MouseEvent<any>) {
|
||||
e && e.stopPropagation();
|
||||
alert('logout');
|
||||
}
|
||||
|
||||
function blockViewCollapse(e?: React.MouseEvent<any>) {
|
||||
e && e.stopPropagation();
|
||||
}
|
||||
|
||||
function getExpandCallback(expand: string, changeExpanded: (expanded: string) => void) {
|
||||
return (e?: React.MouseEvent<any>) => {
|
||||
e && e.stopPropagation();
|
||||
changeExpanded(expand);
|
||||
};
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
@import "~@microsoft/azure-iot-ux-fluent-css/src/typography";
|
Загрузка…
Ссылка в новой задаче