[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:
Vishwam Subramanyam 2019-02-13 16:28:44 -08:00 коммит произвёл GitHub
Родитель 105e798faf
Коммит 3dae340e04
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
29 изменённых файлов: 3551 добавлений и 3845 удалений

Просмотреть файл

@ -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.

Просмотреть файл

@ -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 dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt 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 wont 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).

6362
package-lock.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;
}

11
src/areas/home/home.tsx Normal file
Просмотреть файл

@ -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>
);
}

35
src/errorBoundary.tsx Normal file
Просмотреть файл

@ -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;
}
}

Просмотреть файл

@ -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>
);

33
src/shell/navigation.tsx Normal file
Просмотреть файл

@ -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>
);
}

37
src/shell/routes.tsx Normal file
Просмотреть файл

@ -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>
);
}

49
src/shell/settings.tsx Normal file
Просмотреть файл

@ -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);

124
src/shell/shell.tsx Normal file
Просмотреть файл

@ -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";