Skip to content

Commit

Permalink
feat: common instance context (#238)
Browse files Browse the repository at this point in the history
  • Loading branch information
miralemd committed Dec 13, 2019
1 parent ce43f0c commit 3954705
Show file tree
Hide file tree
Showing 33 changed files with 389 additions and 296 deletions.
7 changes: 5 additions & 2 deletions apis/locale/src/translator.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ const format = (message = '', args = []) => {

export default function translator({ initial = 'en-US', fallback = 'en-US' } = {}) {
const dictionaries = {};
const currentLocale = initial;
let currentLocale = initial;

/**
* @interface Translator
*/
const api = {
language: () => {
language: lang => {
if (lang) {
currentLocale = lang;
}
return currentLocale;
},
/**
Expand Down
16 changes: 8 additions & 8 deletions apis/nucleus/__tests__/unit/selectiontoolbar.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ describe('<SelectionToolbar />', () => {
},
};
const STItem = () => '';
const LocaleContext = React.createContext();
const InstanceContext = React.createContext();
const [{ default: STB }] = aw.mock(
[
['**/SelectionToolbarItem.jsx', () => STItem],
['**/LocaleContext.js', () => LocaleContext],
['**/InstanceContext.js', () => InstanceContext],
],
['../../src/components/SelectionToolbar']
);
Expand All @@ -42,9 +42,9 @@ describe('<SelectionToolbar />', () => {
translator.get.withArgs('Selection.Clear').returns('localized clear');

const c = renderer.create(
<LocaleContext.Provider value={translator}>
<InstanceContext.Provider value={{ translator }}>
<STB api={props.sn.component.selections} xItems={props.sn.selectionToolbar.xItems} />
</LocaleContext.Provider>
</InstanceContext.Provider>
);

items = c.root.findAllByType(STItem);
Expand Down Expand Up @@ -113,11 +113,11 @@ describe('<SelectionToolbar />', () => {
selectionToolbar: { items: [{ key: 'mine' }] },
},
};
const LocaleContext = React.createContext();
const InstanceContext = React.createContext();
const [{ default: STB }] = aw.mock(
[
['**/SelectionToolbarItem.jsx', () => () => ''],
['**/LocaleContext.js', () => LocaleContext],
['**/InstanceContext.js', () => InstanceContext],
],
['../../src/components/SelectionToolbar']
);
Expand All @@ -127,9 +127,9 @@ describe('<SelectionToolbar />', () => {
};

const c = renderer.create(
<LocaleContext.Provider value={translator}>
<InstanceContext.Provider value={{ translator }}>
<STB api={props.sn.component.selections} items={props.sn.selectionToolbar.items} />
</LocaleContext.Provider>
</InstanceContext.Provider>
);

expect(c.toJSON()).to.deep.eql(['', '', '']);
Expand Down
16 changes: 8 additions & 8 deletions apis/nucleus/src/__tests__/app-theme.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ describe('app-theme', () => {

describe('custom', () => {
it('should load and apply custom theme', async () => {
const root = { theme: sandbox.spy() };
const root = { setMuiThemeName: sandbox.spy() };
const at = appThemeFn({
root,
logger,
Expand All @@ -46,15 +46,15 @@ describe('app-theme', () => {
],
});
await at.setTheme('darkish');
expect(root.theme).to.have.been.calledWithExactly('dark');
expect(root.setMuiThemeName).to.have.been.calledWithExactly('dark');
expect(internalAPI.setTheme).to.have.been.calledWithExactly({
type: 'dark',
color: 'red',
});
});

it('should timeout after 5sec', async () => {
const root = { theme: sinon.spy() };
const root = { setMuiThemeName: sinon.spy() };
const at = appThemeFn({
root,
logger,
Expand All @@ -76,21 +76,21 @@ describe('app-theme', () => {

describe('defaults', () => {
it('should apply light theme on React root when themeName is not found', () => {
const root = { theme: sinon.spy() };
const root = { setMuiThemeName: sinon.spy() };
const at = appThemeFn({ root });
at.setTheme('foo');
expect(root.theme).to.have.been.calledWithExactly('light');
expect(root.setMuiThemeName).to.have.been.calledWithExactly('light');
});

it('should apply dark theme on React root when themename is "dark"', () => {
const root = { theme: sinon.spy() };
const root = { setMuiThemeName: sinon.spy() };
const at = appThemeFn({ root });
at.setTheme('dark');
expect(root.theme).to.have.been.calledWithExactly('dark');
expect(root.setMuiThemeName).to.have.been.calledWithExactly('dark');
});

it('should apply "light" as type on internal theme', () => {
const root = { theme: sinon.spy() };
const root = { setMuiThemeName: sinon.spy() };
const at = appThemeFn({ root });
at.setTheme('light');
expect(internalAPI.setTheme).to.have.been.calledWithExactly({
Expand Down
48 changes: 46 additions & 2 deletions apis/nucleus/src/__tests__/nucleus.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@ describe('nucleus', () => {
let createObject;
let getObject;
let sandbox;
let rootApp;
let translator;
before(() => {
sandbox = sinon.createSandbox({ useFakeTimers: true });
createObject = sandbox.stub();
getObject = sandbox.stub();
appThemeFn = sandbox.stub();
rootApp = sandbox.stub();
translator = { add: sandbox.stub(), language: sandbox.stub() };
[{ default: create }] = aw.mock(
[
['**/locale/app-locale.js', () => () => ({ translator: () => ({ add: () => {} }) })],
['**/locale/app-locale.js', () => () => ({ translator })],
['**/selections/index.js', () => ({ createAppSelectionAPI: () => ({}) })],
['**/components/NebulaApp.jsx', () => () => [{}]],
['**/components/NebulaApp.jsx', () => rootApp],
['**/components/selections/AppSelections.jsx', () => () => ({})],
['**/object/create-object.js', () => createObject],
['**/object/get-object.js', () => getObject],
Expand All @@ -28,6 +32,8 @@ describe('nucleus', () => {
beforeEach(() => {
createObject.returns('created object');
getObject.returns('got object');
appThemeFn.returns({ externalAPI: 'internal', setTheme: sandbox.stub() });
rootApp.returns([{}]);
});

afterEach(() => {
Expand Down Expand Up @@ -76,4 +82,42 @@ describe('nucleus', () => {
expect(waited).to.equal(true);
expect(c).to.equal('got object');
});

it('should initite root app with context', () => {
create('app');
expect(rootApp).to.have.been.calledWithExactly({
app: 'app',
context: {
language: 'en-US',
theme: 'light',
permissions: ['idle', 'interact', 'select', 'fetch'],
translator,
},
});
});

it('should only update context when property is known and changed', async () => {
const root = { context: sandbox.stub() };
const theme = { setTheme: sandbox.stub() };

rootApp.returns([root]);
appThemeFn.returns(theme);

const nuked = create('app');
expect(root.context.callCount).to.equal(0);

nuked.context({ foo: 'a' });
expect(root.context.callCount).to.equal(0);

nuked.context({ permissions: 'a' });
expect(root.context.callCount).to.equal(1);

nuked.context({ language: 'sv-SE' });
expect(root.context.callCount).to.equal(2);
expect(translator.language).to.have.been.calledWithExactly('sv-SE');

await nuked.context({ theme: 'sv-SE' });
expect(root.context.callCount).to.equal(3);
expect(theme.setTheme).to.have.been.calledWithExactly('sv-SE');
});
});
4 changes: 2 additions & 2 deletions apis/nucleus/src/app-theme.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default function appTheme({ themes = [], logger, root } = {}) {
} else {
muiTheme = raw.type === 'dark' ? 'dark' : 'light';
theme.internalAPI.setTheme(raw);
root.theme(muiTheme);
root.setMuiThemeName(muiTheme);
}
} catch (e) {
logger.error(e);
Expand All @@ -28,7 +28,7 @@ export default function appTheme({ themes = [], logger, root } = {}) {
theme.internalAPI.setTheme({
type: muiTheme,
});
root.theme(muiTheme);
root.setMuiThemeName(muiTheme);
}
};

Expand Down
6 changes: 3 additions & 3 deletions apis/nucleus/src/components/Cell.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import Supernova from './Supernova';

import useRect from '../hooks/useRect';
import useLayout from '../hooks/useLayout';
import LocaleContext from '../contexts/LocaleContext';
import InstanceContext from '../contexts/InstanceContext';
import { createObjectSelectionAPI } from '../selections';

const initialState = err => ({
Expand Down Expand Up @@ -165,7 +165,7 @@ const Cell = forwardRef(({ corona, model, initialSnContext, initialSnOptions, in
},
} = corona;

const translator = useContext(LocaleContext);
const { translator, language } = useContext(InstanceContext);
const theme = useTheme();
const [state, dispatch] = useReducer(contentReducer, initialState(initialError));
const [layout, validating, cancel, retry] = useLayout({ app, model });
Expand Down Expand Up @@ -220,7 +220,7 @@ const Cell = forwardRef(({ corona, model, initialSnContext, initialSnOptions, in
load(layout, withVersion);

return () => {};
}, [types, state.sn, model, layout]);
}, [types, state.sn, model, layout, language]);

// Long running query
useEffect(() => {
Expand Down
4 changes: 2 additions & 2 deletions apis/nucleus/src/components/LongRunningQuery.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import React, { useState, useContext } from 'react';
import { makeStyles, Grid, Typography, Button } from '@material-ui/core';
import WarningTriangle from '@nebula.js/ui/icons/warning-triangle-2';
import LocaleContext from '../contexts/LocaleContext';
import InstanceContext from '../contexts/InstanceContext';

import Progress from './Progress';

Expand Down Expand Up @@ -65,7 +65,7 @@ export default function LongRunningQuery({ onCancel, onRetry }) {
const { stripes, cancel, retry } = useStyles();
const [canCancel, setCanCancel] = useState(!!onCancel);
const [canRetry, setCanRetry] = useState(!!onRetry);
const translator = useContext(LocaleContext);
const { translator } = useContext(InstanceContext);

const handleCancel = () => {
setCanCancel(false);
Expand Down
45 changes: 19 additions & 26 deletions apis/nucleus/src/components/NebulaApp.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,25 @@ import ReactDOM from 'react-dom';

import { createTheme, ThemeProvider, StylesProvider, createGenerateClassName } from '@nebula.js/ui/theme';

import LocaleContext from '../contexts/LocaleContext';
import DirectionContext from '../contexts/DirectionContext';
import InstanceContext from '../contexts/InstanceContext';

const THEME_PREFIX = (process.env.NEBULA_VERSION || '').replace(/[.-]/g, '_');

let counter = 0;

const NebulaApp = forwardRef(({ translator }, ref) => {
const [d, setDirection] = useState();
const [tn, setThemeName] = useState();
const NebulaApp = forwardRef(({ initialContext }, ref) => {
const [context, setContext] = useState(initialContext);
const [muiThemeName, setMuiThemeName] = useState();
const { theme, generator } = useMemo(
() => ({
theme: createTheme(tn),
theme: createTheme(muiThemeName),
generator: createGenerateClassName({
productionPrefix: `${THEME_PREFIX}-`,
disableGlobal: true,
seed: `nebulajs-${counter++}`,
}),
}),
[tn]
[muiThemeName]
);

const [components, setComponents] = useState([]);
Expand All @@ -38,28 +37,26 @@ const NebulaApp = forwardRef(({ translator }, ref) => {
setComponents([...components]);
}
},
setThemeName(name) {
setThemeName(name);
setMuiThemeName(name) {
setMuiThemeName(name);
},
setDirection(dir) {
setDirection(dir);
setContext(ctx) {
setContext(ctx);
},
}));

return (
<StylesProvider generateClassName={generator}>
<ThemeProvider theme={theme}>
<LocaleContext.Provider value={translator}>
<DirectionContext.Provider value={d}>
<>{components}</>
</DirectionContext.Provider>
</LocaleContext.Provider>
<InstanceContext.Provider value={context}>
<>{components}</>
</InstanceContext.Provider>
</ThemeProvider>
</StylesProvider>
);
});

export default function boot({ app, theme: themeName = 'light', translator, direction }) {
export default function boot({ app, context }) {
let resolveRender;
const rendered = new Promise(resolve => {
resolveRender = resolve;
Expand All @@ -71,11 +68,7 @@ export default function boot({ app, theme: themeName = 'light', translator, dire
element.setAttribute('data-app-id', app.id);
document.body.appendChild(element);

ReactDOM.render(
<NebulaApp ref={appRef} themeName={themeName} translator={translator} direction={direction} />,
element,
resolveRender
);
ReactDOM.render(<NebulaApp ref={appRef} initialContext={context} />, element, resolveRender);

return [
{
Expand All @@ -91,16 +84,16 @@ export default function boot({ app, theme: themeName = 'light', translator, dire
appRef.current.removeComponent(component);
})();
},
theme(name) {
setMuiThemeName(themeName) {
(async () => {
await rendered;
appRef.current.setThemeName(name);
appRef.current.setMuiThemeName(themeName);
})();
},
direction(d) {
context(ctx) {
(async () => {
await rendered;
appRef.current.setDirection(d);
appRef.current.setContext(ctx);
})();
},
},
Expand Down
4 changes: 2 additions & 2 deletions apis/nucleus/src/components/SelectionToolbar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { close } from '@nebula.js/ui/icons/close';
import { tick } from '@nebula.js/ui/icons/tick';
import { clearSelections } from '@nebula.js/ui/icons/clear-selections';

import LocaleContext from '../contexts/LocaleContext';
import InstanceContext from '../contexts/InstanceContext';
import Item from './SelectionToolbarItem';

const SelectionToolbar = React.forwardRef(({ layout, items }, ref) => {
Expand All @@ -17,7 +17,7 @@ const SelectionToolbar = React.forwardRef(({ layout, items }, ref) => {
});

const SelectionToolbarWithDefault = ({ layout, api, xItems = [], onCancel = () => {}, onConfirm = () => {} }) => {
const translator = useContext(LocaleContext);
const { translator } = useContext(InstanceContext);

const items = [
...xItems,
Expand Down
Loading

0 comments on commit 3954705

Please sign in to comment.