mirror of
https://github.com/qlik-oss/nebula.js.git
synced 2025-12-19 09:48:18 -05:00
refactor(nucleus): use portals when mounting entry components
This commit is contained in:
75
packages/nucleus/src/components/App.jsx
Normal file
75
packages/nucleus/src/components/App.jsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import React, {
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import {
|
||||
createTheme,
|
||||
ThemeProvider,
|
||||
StylesProvider,
|
||||
createGenerateClassName,
|
||||
} from '@nebula.js/ui/theme';
|
||||
|
||||
const THEME_PREFIX = (process.env.NEBULA_VERSION || '').replace(/[.-]/g, '_');
|
||||
|
||||
let counter = 0;
|
||||
|
||||
function App({
|
||||
children,
|
||||
}) {
|
||||
const { theme, generator } = useMemo(() => ({
|
||||
theme: createTheme(),
|
||||
generator: createGenerateClassName({
|
||||
productionPrefix: `${THEME_PREFIX}-`,
|
||||
disableGlobal: true,
|
||||
seed: `nebulajs-${counter++}`,
|
||||
}),
|
||||
}), []);
|
||||
|
||||
return (
|
||||
<StylesProvider generateClassName={generator}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<>
|
||||
{children}
|
||||
</>
|
||||
</ThemeProvider>
|
||||
</StylesProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default function boot({
|
||||
app,
|
||||
}) {
|
||||
const element = document.createElement('div');
|
||||
element.style.display = 'none';
|
||||
element.setAttribute('data-nebulajs-version', process.env.NEBULA_VERSION || '');
|
||||
element.setAttribute('data-app-id', app.id);
|
||||
document.body.appendChild(element);
|
||||
const components = [];
|
||||
|
||||
const update = () => {
|
||||
ReactDOM.render(
|
||||
<App app={app}>{components}</App>,
|
||||
element,
|
||||
);
|
||||
};
|
||||
|
||||
// const unmount = () => {
|
||||
// ReactDOM.unmountComponentAtNode(element);
|
||||
// };
|
||||
|
||||
update();
|
||||
|
||||
return {
|
||||
add(component) {
|
||||
components.push(component);
|
||||
update();
|
||||
},
|
||||
remove(component) {
|
||||
const idx = components.indexOf(component);
|
||||
if (idx !== -1) {
|
||||
components.splice(idx, 1);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,14 +1,8 @@
|
||||
import React, {
|
||||
useMemo,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
createTheme,
|
||||
ThemeProvider,
|
||||
StylesProvider,
|
||||
createGenerateClassName,
|
||||
} from '@nebula.js/ui/theme';
|
||||
|
||||
import {
|
||||
Grid,
|
||||
} from '@nebula.js/ui/components';
|
||||
@@ -20,12 +14,6 @@ import Footer from './Footer';
|
||||
import Supernova from './Supernova';
|
||||
import Placeholder from './Placeholder';
|
||||
|
||||
const generateClassName = createGenerateClassName({
|
||||
productionPrefix: 'cell-',
|
||||
disableGlobal: true,
|
||||
seed: 'nebula',
|
||||
});
|
||||
|
||||
const showRequirements = (sn, layout) => {
|
||||
if (!sn || !sn.generator || !sn.generator.qae || !layout || !layout.qHyperCube) {
|
||||
return false;
|
||||
@@ -58,40 +46,50 @@ const Content = ({ children }) => (
|
||||
);
|
||||
|
||||
export default function Cell({
|
||||
objectProps,
|
||||
userProps,
|
||||
themeDefinition = {},
|
||||
api,
|
||||
}) {
|
||||
const theme = useMemo(() => createTheme(themeDefinition), [JSON.stringify(themeDefinition)]);
|
||||
const SN = (showRequirements(objectProps.sn, objectProps.layout) ? Requirements : Supernova);
|
||||
const Comp = !objectProps.sn ? Placeholder : SN;
|
||||
const err = objectProps.error || false;
|
||||
const [state, setState] = useState({
|
||||
objectProps: api.objectProps(),
|
||||
userProps: api.userProps(),
|
||||
});
|
||||
useEffect(() => {
|
||||
const onChanged = () => {
|
||||
setState({
|
||||
objectProps: api.objectProps(),
|
||||
userProps: api.userProps(),
|
||||
});
|
||||
};
|
||||
api.on('changed', onChanged);
|
||||
return () => {
|
||||
api.removeListener('changed', onChanged);
|
||||
};
|
||||
}, [api]);
|
||||
|
||||
const SN = (showRequirements(state.objectProps.sn, state.objectProps.layout) ? Requirements : Supernova);
|
||||
const Comp = !state.objectProps.sn ? Placeholder : SN;
|
||||
const err = state.objectProps.error || false;
|
||||
return (
|
||||
<StylesProvider generateClassName={generateClassName}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<Grid container direction="column" spacing={0} style={{ height: '100%', padding: '8px', boxSixing: 'borderBox' }}>
|
||||
<Grid item style={{ maxWidth: '100%' }}>
|
||||
<Header layout={objectProps.layout} sn={objectProps.sn}> </Header>
|
||||
</Grid>
|
||||
<Grid item xs>
|
||||
<Content>
|
||||
{err
|
||||
? (<CError {...err} />)
|
||||
: (
|
||||
<Comp
|
||||
key={objectProps.layout.visualization}
|
||||
sn={objectProps.sn}
|
||||
snContext={userProps.context}
|
||||
snOptions={userProps.options}
|
||||
layout={objectProps.layout}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</Content>
|
||||
</Grid>
|
||||
<Footer layout={objectProps.layout} />
|
||||
</Grid>
|
||||
</ThemeProvider>
|
||||
</StylesProvider>
|
||||
<Grid container direction="column" spacing={0} style={{ height: '100%', padding: '8px', boxSixing: 'borderBox' }}>
|
||||
<Grid item style={{ maxWidth: '100%' }}>
|
||||
<Header layout={state.objectProps.layout} sn={state.objectProps.sn}> </Header>
|
||||
</Grid>
|
||||
<Grid item xs>
|
||||
<Content>
|
||||
{err
|
||||
? (<CError {...err} />)
|
||||
: (
|
||||
<Comp
|
||||
key={state.objectProps.layout.visualization}
|
||||
sn={state.objectProps.sn}
|
||||
snContext={state.userProps.context}
|
||||
snOptions={state.userProps.options}
|
||||
layout={state.objectProps.layout}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</Content>
|
||||
</Grid>
|
||||
<Footer layout={state.objectProps.layout} />
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,14 +15,14 @@ const Header = ({
|
||||
const showSubtitle = layout && layout.showTitles && !!layout.subtitle;
|
||||
const showInSelectionActions = sn && layout && layout.qSelectionInfo && layout.qSelectionInfo.qInSelections;
|
||||
return (
|
||||
<Grid container wrap="nowrap" style={{ flexGrow: 0, flexWrap: 'nowrap' }}>
|
||||
<Grid item wrap="nowrap" style={{ flexGrow: 1, minWidth: 0 }}>
|
||||
<Grid container direction="column" style={{ flexGrow: 1, flexWrap: 'nowrap', minWidth: 0 }}>
|
||||
<Grid container wrap="nowrap" style={{ flexGrow: 0 }}>
|
||||
<Grid item zeroMinWidth xs>
|
||||
<Grid container wrap="nowrap" direction="column">
|
||||
{showTitle && (<Typography variant="h6" noWrap>{layout.title}</Typography>)}
|
||||
{showSubtitle && (<Typography variant="body2" noWrap>{layout.subtitle}</Typography>)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item style={{ flexWrap: 'nowrap', whiteSpace: 'nowrap', minHeight: '32px' }}>
|
||||
<Grid item style={{ whiteSpace: 'nowrap', minHeight: '32px' }}>
|
||||
{showInSelectionActions && (<SelectionToolbar inline sn={sn} />)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -5,32 +5,30 @@ import Cell from './Cell';
|
||||
export default function boot({
|
||||
element,
|
||||
model,
|
||||
}, props, config) {
|
||||
ReactDOM.render(
|
||||
api,
|
||||
nebulaContext,
|
||||
}) {
|
||||
const {
|
||||
root,
|
||||
} = nebulaContext;
|
||||
|
||||
const portal = ReactDOM.createPortal(
|
||||
<Cell
|
||||
{...props}
|
||||
api={api}
|
||||
model={model}
|
||||
/>,
|
||||
element,
|
||||
);
|
||||
|
||||
const unmount = () => {
|
||||
ReactDOM.unmountComponentAtNode(element);
|
||||
};
|
||||
|
||||
const update = (p) => {
|
||||
ReactDOM.render(<Cell
|
||||
{...p}
|
||||
model={model}
|
||||
/>, element);
|
||||
root.remove(portal);
|
||||
};
|
||||
|
||||
model.once('closed', unmount);
|
||||
|
||||
return config.env.Promise.resolve({
|
||||
setProps(p) {
|
||||
update(p);
|
||||
},
|
||||
unmount,
|
||||
});
|
||||
root.add(portal);
|
||||
|
||||
return () => {
|
||||
unmount();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import React, {
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import {
|
||||
@@ -8,52 +6,39 @@ import {
|
||||
} from '@nebula.js/ui/components';
|
||||
|
||||
import {
|
||||
createTheme,
|
||||
ThemeProvider,
|
||||
StylesProvider,
|
||||
createGenerateClassName,
|
||||
useTheme,
|
||||
} from '@nebula.js/ui/theme';
|
||||
|
||||
import SelectedFields from './SelectedFields';
|
||||
import Nav from './Nav';
|
||||
|
||||
const generateClassName = createGenerateClassName({
|
||||
productionPrefix: 'sel-',
|
||||
disableGlobal: true,
|
||||
seed: 'nebula',
|
||||
});
|
||||
|
||||
export function AppSelections({
|
||||
api,
|
||||
}) {
|
||||
const theme = useMemo(() => createTheme(), []);
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<StylesProvider generateClassName={generateClassName}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<Grid
|
||||
container
|
||||
spacing={0}
|
||||
wrap="nowrap"
|
||||
style={{
|
||||
backgroundColor: '#fff',
|
||||
minHeight: '40px',
|
||||
}}
|
||||
>
|
||||
<Grid
|
||||
item
|
||||
style={{
|
||||
borderRight: `1px solid ${theme.palette.divider}`,
|
||||
}}
|
||||
>
|
||||
<Nav api={api} />
|
||||
</Grid>
|
||||
<Grid item xs style={{ backgroundColor: '#E5E5E5', overflow: 'hidden' }}>
|
||||
<SelectedFields api={api} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ThemeProvider>
|
||||
</StylesProvider>
|
||||
<Grid
|
||||
container
|
||||
spacing={0}
|
||||
wrap="nowrap"
|
||||
style={{
|
||||
backgroundColor: '#fff',
|
||||
minHeight: '40px',
|
||||
}}
|
||||
>
|
||||
<Grid
|
||||
item
|
||||
style={{
|
||||
borderRight: `1px solid ${theme.palette.divider}`,
|
||||
}}
|
||||
>
|
||||
<Nav api={api} />
|
||||
</Grid>
|
||||
<Grid item xs style={{ backgroundColor: '#E5E5E5', overflow: 'hidden' }}>
|
||||
<SelectedFields api={api} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,18 +46,10 @@ export default function mount({
|
||||
element,
|
||||
api,
|
||||
}) {
|
||||
ReactDOM.render(
|
||||
return ReactDOM.createPortal(
|
||||
<AppSelections
|
||||
api={api}
|
||||
/>,
|
||||
element,
|
||||
);
|
||||
|
||||
const unmount = () => {
|
||||
ReactDOM.unmountComponentAtNode(element);
|
||||
};
|
||||
|
||||
return () => {
|
||||
unmount();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
/* eslint no-underscore-dangle:0 */
|
||||
|
||||
import { createAppSelectionAPI } from './selections';
|
||||
|
||||
import App from './components/App';
|
||||
import AppSelectionsPortal from './components/selections/AppSelections';
|
||||
|
||||
import create from './object/create-object';
|
||||
import get from './object/get-object';
|
||||
import types from './sn/types';
|
||||
@@ -17,14 +22,22 @@ function apiGenerator(app) {
|
||||
load: () => undefined,
|
||||
};
|
||||
|
||||
const root = App({
|
||||
app,
|
||||
});
|
||||
|
||||
const context = {
|
||||
nebbie: null,
|
||||
app,
|
||||
config,
|
||||
logger: lgr,
|
||||
types: types(config),
|
||||
root,
|
||||
};
|
||||
|
||||
let selectionsApi = null;
|
||||
let selectionsComponentReference = null;
|
||||
|
||||
const api = {
|
||||
get: (getCfg, userProps) => get(getCfg, userProps, context),
|
||||
create: (createCfg, userProps) => create(createCfg, userProps, context),
|
||||
@@ -36,7 +49,31 @@ function apiGenerator(app) {
|
||||
config.load = $;
|
||||
return api;
|
||||
},
|
||||
selections: () => app._selections, // eslint-disable-line no-underscore-dangle
|
||||
selections: () => {
|
||||
if (!selectionsApi) {
|
||||
selectionsApi = {
|
||||
...app._selections, // eslint-disable-line no-underscore-dangle
|
||||
mount(element) {
|
||||
if (selectionsComponentReference) {
|
||||
console.error('Already mounted');
|
||||
return;
|
||||
}
|
||||
selectionsComponentReference = AppSelectionsPortal({
|
||||
element,
|
||||
api: app._selections,
|
||||
});
|
||||
root.add(selectionsComponentReference);
|
||||
},
|
||||
unmount() {
|
||||
if (selectionsComponentReference) {
|
||||
root.remove(selectionsComponentReference);
|
||||
selectionsComponentReference = null;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
return selectionsApi;
|
||||
},
|
||||
types: context.types,
|
||||
};
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function initiate(getCfg, optional, context) {
|
||||
return context.app.getObject(getCfg.id).then((model) => {
|
||||
const viz = vizualizationAPI({
|
||||
model,
|
||||
config: context.config,
|
||||
context,
|
||||
});
|
||||
|
||||
const objectAPI = new ObjectAPI(model, context, viz);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint no-underscore-dangle: 0 */
|
||||
import eventmixin from './event-mixin';
|
||||
import visual from '../components/selections/AppSelections';
|
||||
// import visual from '../components/selections/AppSelections';
|
||||
|
||||
import modelCache from '../object/model-cache';
|
||||
import { observe } from '../object/observer';
|
||||
@@ -13,7 +13,7 @@ const create = (app) => {
|
||||
let canClear = false;
|
||||
|
||||
let modalObject;
|
||||
let mounted;
|
||||
// let mounted;
|
||||
let lyt;
|
||||
const api = {
|
||||
model: app,
|
||||
@@ -75,21 +75,6 @@ const create = (app) => {
|
||||
clearField(field, state = '$') {
|
||||
return app.getField(field, state).then(f => f.clear());
|
||||
},
|
||||
mount(element) {
|
||||
if (mounted) {
|
||||
console.error('Already mounted');
|
||||
return;
|
||||
}
|
||||
mounted = visual({
|
||||
element,
|
||||
api,
|
||||
});
|
||||
},
|
||||
unmount() {
|
||||
if (mounted) {
|
||||
mounted();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
eventmixin(api);
|
||||
|
||||
@@ -3,17 +3,17 @@ import cell from './components/boot';
|
||||
import { get } from './object/observer';
|
||||
import getPatches from './utils/patcher';
|
||||
|
||||
const noopi = Promise.resolve({
|
||||
setProps() {},
|
||||
unmount() {},
|
||||
});
|
||||
import eventMixin from './selections/event-mixin';
|
||||
|
||||
const noopi = () => {};
|
||||
|
||||
export default function ({
|
||||
model,
|
||||
config,
|
||||
context,
|
||||
initialUserProps = {},
|
||||
} = {}) {
|
||||
let c = noopi;
|
||||
let reference = noopi;
|
||||
let elementReference = null;
|
||||
|
||||
let userProps = {
|
||||
options: {},
|
||||
@@ -29,6 +29,17 @@ export default function ({
|
||||
error: null,
|
||||
};
|
||||
|
||||
const cellApi = {
|
||||
userProps() {
|
||||
return userProps;
|
||||
},
|
||||
objectProps() {
|
||||
return objectProps;
|
||||
},
|
||||
};
|
||||
|
||||
eventMixin(cellApi);
|
||||
|
||||
let queueShow = false;
|
||||
|
||||
const mount = (element) => {
|
||||
@@ -36,13 +47,13 @@ export default function ({
|
||||
queueShow = element;
|
||||
return;
|
||||
}
|
||||
c = cell({
|
||||
elementReference = element;
|
||||
reference = cell({
|
||||
element,
|
||||
model,
|
||||
}, {
|
||||
userProps,
|
||||
objectProps,
|
||||
}, config);
|
||||
api: cellApi,
|
||||
nebulaContext: context,
|
||||
});
|
||||
};
|
||||
|
||||
const update = () => {
|
||||
@@ -50,10 +61,7 @@ export default function ({
|
||||
mount(queueShow);
|
||||
queueShow = false;
|
||||
}
|
||||
c.then(x => x.setProps({
|
||||
objectProps,
|
||||
userProps,
|
||||
}));
|
||||
cellApi.emit('changed');
|
||||
};
|
||||
|
||||
const setUserProps = (up) => {
|
||||
@@ -82,8 +90,8 @@ export default function ({
|
||||
close() {
|
||||
// TODO - destroy session object (if created as such)
|
||||
model.emit('closed');
|
||||
c.then(x => x.unmount());
|
||||
c = noopi;
|
||||
reference();
|
||||
reference = noopi;
|
||||
},
|
||||
setTemporaryProperties(props) {
|
||||
return get(model, 'effectiveProperties').then((current) => {
|
||||
@@ -107,34 +115,32 @@ export default function ({
|
||||
return api;
|
||||
},
|
||||
takeSnapshot() {
|
||||
return c.then((x) => {
|
||||
if (x.reference) {
|
||||
const content = x.reference.querySelector('.nebulajs-sn');
|
||||
if (content) {
|
||||
const rect = content.getBoundingClientRect();
|
||||
if (objectProps.sn) {
|
||||
const snapshot = {
|
||||
...objectProps.layout,
|
||||
snapshotData: {
|
||||
object: {
|
||||
size: {
|
||||
w: rect.width,
|
||||
h: rect.height,
|
||||
},
|
||||
if (elementReference) {
|
||||
const content = elementReference.querySelector('.nebulajs-sn');
|
||||
if (content) {
|
||||
const rect = content.getBoundingClientRect();
|
||||
if (objectProps.sn) {
|
||||
const snapshot = {
|
||||
...objectProps.layout,
|
||||
snapshotData: {
|
||||
object: {
|
||||
size: {
|
||||
w: rect.width,
|
||||
h: rect.height,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
if (typeof objectProps.sn.component.setSnapshotData === 'function') {
|
||||
return objectProps.sn.component.setSnapshotData(snapshot);
|
||||
}
|
||||
return Promise.resolve(snapshot);
|
||||
if (typeof objectProps.sn.component.setSnapshotData === 'function') {
|
||||
return objectProps.sn.component.setSnapshotData(snapshot);
|
||||
}
|
||||
return Promise.reject(new Error('No content'));
|
||||
return Promise.resolve(snapshot);
|
||||
}
|
||||
return Promise.reject(new Error('No content'));
|
||||
}
|
||||
return Promise.reject(new Error('Not mounted yet'));
|
||||
});
|
||||
}
|
||||
return Promise.reject(new Error('Not mounted yet'));
|
||||
},
|
||||
|
||||
// QVisualization API
|
||||
|
||||
@@ -91,6 +91,7 @@ const config = (isEsm) => {
|
||||
plugins: [
|
||||
replace({
|
||||
'process.env.NODE_ENV': JSON.stringify(isEsm ? 'development' : 'production'),
|
||||
'process.env.NEBULA_VERSION': JSON.stringify(version),
|
||||
}),
|
||||
nodeResolve({
|
||||
extensions: ['.js', '.jsx'],
|
||||
|
||||
Reference in New Issue
Block a user