mirror of
https://github.com/qlik-oss/nebula.js.git
synced 2025-12-19 09:48:18 -05:00
feat: sheet embed support (#1013)
* feat: add basic sheet rendering support * chore: add missing file * fix: correct bg colors for none support * chore: fix test that relied on dark bg * chore: fix ref * chore: api spec update * chore: add todo comments * chore: use memo * chore: a bit less verbose * chore: list * chore: cleaning * chore: add rendering test * chore: enable rendering test * chore: settings * chore: settings * chore: disable rendering tests * chore: revert test tests
This commit is contained in:
@@ -110,6 +110,9 @@ jobs:
|
||||
fi
|
||||
- store_test_results:
|
||||
path: coverage/junit
|
||||
# - run:
|
||||
# name: Test rendering
|
||||
# command: yarn run test:rendering
|
||||
- run:
|
||||
name: Test component
|
||||
command: yarn run test:component --chrome.browserWSEndpoint "ws://localhost:3000" --no-launch
|
||||
@@ -119,6 +122,7 @@ jobs:
|
||||
- run:
|
||||
name: Test integration
|
||||
command: yarn run test:integration --chrome.browserWSEndpoint "ws://localhost:3000" --no-launch
|
||||
|
||||
- nebula_create:
|
||||
project_name: 'generated/hello'
|
||||
picasso_template: 'none'
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,6 +2,7 @@
|
||||
*.rej
|
||||
*.tmp
|
||||
*.log
|
||||
*.pem
|
||||
.cache/
|
||||
.DS_Store
|
||||
.idea/
|
||||
|
||||
@@ -67,6 +67,7 @@ function createMock(genericObject, options) {
|
||||
}),
|
||||
{}
|
||||
),
|
||||
genericType: genericObject.type,
|
||||
};
|
||||
return { [qId]: mock };
|
||||
}
|
||||
|
||||
@@ -279,7 +279,7 @@ const Cell = forwardRef(
|
||||
const { nebbie } = halo.public;
|
||||
const { disableCellPadding = false } = halo.context || {};
|
||||
|
||||
const { translator, language, keyboardNavigation } = useContext(InstanceContext);
|
||||
const { theme: themeName, translator, language, keyboardNavigation } = useContext(InstanceContext);
|
||||
const theme = useTheme();
|
||||
const [cellRef, cellRect, cellNode] = useRect();
|
||||
const [state, dispatch] = useReducer(contentReducer, initialState(initialError));
|
||||
@@ -289,8 +289,7 @@ const Cell = forwardRef(
|
||||
const [snOptions, setSnOptions] = useState(initialSnOptions);
|
||||
const [snPlugins, setSnPlugins] = useState(initialSnPlugins);
|
||||
const cellElementId = `njs-cell-${currentId}`;
|
||||
const clickOutElements = [`#${cellElementId}`, '.njs-action-toolbar-popover']; // elements which will not trigger the click out listener
|
||||
const [selections] = useObjectSelections(app, model, clickOutElements);
|
||||
const [selections] = useObjectSelections(app, model, [`#${cellElementId}`, '.njs-action-toolbar-popover']); // elements which will not trigger the click out listener
|
||||
const [hovering, setHover] = useState(false);
|
||||
const hoveringDebouncer = useRef({ enter: null, leave: null });
|
||||
const [bgColor, setBgColor] = useState(undefined);
|
||||
@@ -311,7 +310,7 @@ const Cell = forwardRef(
|
||||
const bgComp = layout?.components ? layout.components.find((comp) => comp.key === 'general') : null;
|
||||
setBgColor(resolveBgColor(bgComp, halo.public.theme));
|
||||
setBgImage(resolveBgImage(bgComp, halo.app));
|
||||
}, [layout, halo.public.theme, halo.app]);
|
||||
}, [layout, halo.public.theme, halo.app, themeName]);
|
||||
|
||||
focusHandler.current.blurCallback = (resetFocus) => {
|
||||
halo.root.toggleFocusOfCells();
|
||||
@@ -495,7 +494,7 @@ const Cell = forwardRef(
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
backgroundColor: bgColor,
|
||||
backgroundColor: bgColor || 'unset',
|
||||
backgroundImage: bgImage && bgImage.url ? `url(${bgImage.url})` : undefined,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundSize: bgImage && bgImage.size,
|
||||
|
||||
@@ -75,6 +75,9 @@ export default function boot({ app, context }) {
|
||||
addCell(id, cell) {
|
||||
cells[id] = cell;
|
||||
},
|
||||
removeCell(id) {
|
||||
delete cells[id];
|
||||
},
|
||||
add(component) {
|
||||
(async () => {
|
||||
await rendered;
|
||||
|
||||
148
apis/nucleus/src/components/Sheet.jsx
Normal file
148
apis/nucleus/src/components/Sheet.jsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import React, { useEffect, useState, useContext, useMemo } from 'react';
|
||||
import useLayout from '../hooks/useLayout';
|
||||
import getObject from '../object/get-object';
|
||||
import Cell from './Cell';
|
||||
import uid from '../object/uid';
|
||||
import { resolveBgColor, resolveBgImage } from '../utils/background-props';
|
||||
import InstanceContext from '../contexts/InstanceContext';
|
||||
|
||||
/**
|
||||
* @interface
|
||||
* @extends HTMLElement
|
||||
* @experimental
|
||||
* @since 3.1.0
|
||||
*/
|
||||
const SheetElement = {
|
||||
/** @type {'njs-sheet'} */
|
||||
className: 'njs-sheet',
|
||||
};
|
||||
|
||||
function getCellRenderer(cell, halo, initialSnOptions, initialSnPlugins, initialError, onMount) {
|
||||
const { x, y, width, height } = cell.bounds;
|
||||
return (
|
||||
<div style={{ left: `${x}%`, top: `${y}%`, width: `${width}%`, height: `${height}%`, position: 'absolute' }}>
|
||||
<Cell
|
||||
ref={cell.cellRef}
|
||||
halo={halo}
|
||||
model={cell.model}
|
||||
currentId={cell.currentId}
|
||||
initialSnOptions={initialSnOptions}
|
||||
initialSnPlugins={initialSnPlugins}
|
||||
initialError={initialError}
|
||||
onMount={onMount}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Sheet({ model, halo, initialSnOptions, initialSnPlugins, initialError, onMount }) {
|
||||
const { root } = halo;
|
||||
const [layout] = useLayout(model);
|
||||
const { theme: themeName } = useContext(InstanceContext);
|
||||
const [cells, setCells] = useState([]);
|
||||
const [bgColor, setBgColor] = useState(undefined);
|
||||
const [bgImage, setBgImage] = useState(undefined);
|
||||
const [deepHash, setDeepHash] = useState('');
|
||||
|
||||
/// For each object
|
||||
useEffect(() => {
|
||||
if (layout) {
|
||||
const hash = JSON.stringify(layout.cells);
|
||||
if (hash === deepHash) {
|
||||
return;
|
||||
}
|
||||
setDeepHash(hash);
|
||||
const fetchObjects = async () => {
|
||||
/*
|
||||
Need to always fetch and evaluate everything as the sheet need to support multiple instances of the same object?
|
||||
No, there is no way to add the same chart twice, so the optimization should be worth it.
|
||||
*/
|
||||
|
||||
// Clear the cell list
|
||||
cells.forEach((c) => {
|
||||
root.removeCell(c.currentId);
|
||||
});
|
||||
|
||||
const lCells = layout.cells;
|
||||
// TODO - should try reuse existing objects on subsequent renders
|
||||
// Non-id updates should only change the "css"
|
||||
const cs = await Promise.all(
|
||||
lCells.map(async (c) => {
|
||||
let mounted;
|
||||
const mountedPromise = new Promise((resolve) => {
|
||||
mounted = resolve;
|
||||
});
|
||||
|
||||
const cell = cells.find((ce) => ce.id === c.name);
|
||||
if (cell) {
|
||||
cell.bounds = c.bounds;
|
||||
delete cell.mountedPromise;
|
||||
return cell;
|
||||
}
|
||||
const vs = await getObject({ id: c.name }, halo);
|
||||
return {
|
||||
model: vs.model,
|
||||
id: c.name,
|
||||
bounds: c.bounds,
|
||||
cellRef: React.createRef(),
|
||||
currentId: uid(),
|
||||
mounted,
|
||||
mountedPromise,
|
||||
};
|
||||
})
|
||||
);
|
||||
cs.forEach((c) => root.addCell(c.currentId, c.cellRef));
|
||||
setCells(cs);
|
||||
};
|
||||
fetchObjects();
|
||||
}
|
||||
}, [layout]);
|
||||
|
||||
const cellRenderers = useMemo(
|
||||
() =>
|
||||
cells
|
||||
? cells.map((c) => getCellRenderer(c, halo, initialSnOptions, initialSnPlugins, initialError, c.mounted))
|
||||
: [],
|
||||
[cells]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const bgComp = layout?.components ? layout.components.find((comp) => comp.key === 'general') : null;
|
||||
setBgColor(resolveBgColor(bgComp, halo.public.theme));
|
||||
setBgImage(resolveBgImage(bgComp, halo.app));
|
||||
}, [layout, halo.public.theme, halo.app, themeName]);
|
||||
|
||||
/* TODO
|
||||
- sheet title + bg + logo etc + as option
|
||||
- sheet exposed classnames for theming
|
||||
*/
|
||||
|
||||
const height = !layout || Number.isNaN(layout.height) ? '100%' : `${Number(layout.height)}%`;
|
||||
const promises = cells.map((c) => c.mountedPromise);
|
||||
const ps = promises.filter((p) => !!p);
|
||||
if (ps.length) {
|
||||
Promise.all(promises).then(() => {
|
||||
// TODO - correct? Currently called each time a new cell is mounted?
|
||||
onMount();
|
||||
});
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={SheetElement.className}
|
||||
style={{
|
||||
width: `100%`,
|
||||
height,
|
||||
position: 'relative',
|
||||
backgroundColor: bgColor,
|
||||
backgroundImage: bgImage && bgImage.url ? `url(${bgImage.url})` : undefined,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundSize: bgImage && bgImage.size,
|
||||
backgroundPosition: bgImage && bgImage.pos,
|
||||
}}
|
||||
>
|
||||
{cellRenderers}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Sheet;
|
||||
31
apis/nucleus/src/components/sheetGlue.jsx
Normal file
31
apis/nucleus/src/components/sheetGlue.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Sheet from './Sheet';
|
||||
|
||||
export default function glue({ halo, element, model, initialSnOptions, initialSnPlugins, onMount, initialError }) {
|
||||
const { root } = halo;
|
||||
const sheetRef = React.createRef();
|
||||
const portal = ReactDOM.createPortal(
|
||||
<Sheet
|
||||
ref={sheetRef}
|
||||
halo={halo}
|
||||
model={model}
|
||||
initialSnOptions={initialSnOptions}
|
||||
initialSnPlugins={initialSnPlugins}
|
||||
initialError={initialError}
|
||||
onMount={onMount}
|
||||
/>,
|
||||
element,
|
||||
model.id
|
||||
);
|
||||
|
||||
const unmount = () => {
|
||||
root.remove(portal);
|
||||
model.removeListener('closed', unmount);
|
||||
};
|
||||
|
||||
model.on('closed', unmount);
|
||||
|
||||
root.add(portal);
|
||||
return [unmount, sheetRef];
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import AppSelectionsPortal from './components/selections/AppSelections';
|
||||
import ListBoxPortal from './components/listbox/ListBoxPortal';
|
||||
|
||||
import create from './object/create-session-object';
|
||||
import get from './object/get-object';
|
||||
import get from './object/get-generic-object';
|
||||
import flagsFn from './flags/flags';
|
||||
import { create as typesFn } from './sn/types';
|
||||
|
||||
@@ -255,9 +255,10 @@ function nuked(configuration = {}) {
|
||||
*/
|
||||
const api = /** @lends Embed# */ {
|
||||
/**
|
||||
* Renders a visualization into an HTMLElement.
|
||||
* Renders a visualization or sheet into an HTMLElement.
|
||||
* Support for sense sheets is experimental.
|
||||
* @param {CreateConfig | GetConfig} cfg - The render configuration.
|
||||
* @returns {Promise<Viz>} A controller to the rendered visualization.
|
||||
* @returns {Promise<Viz|Sheet>} A controller to the rendered visualization or sheet.
|
||||
* @example
|
||||
* // render from existing object
|
||||
* n.render({
|
||||
|
||||
35
apis/nucleus/src/object/get-generic-object.js
Normal file
35
apis/nucleus/src/object/get-generic-object.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import init from './initiate';
|
||||
import initSheet from './initiate-sheet';
|
||||
import { modelStore, rpcRequestModelStore } from '../stores/model-store';
|
||||
|
||||
/**
|
||||
* @interface BaseConfig
|
||||
* @description Basic rendering configuration for rendering an object
|
||||
* @property {HTMLElement} element
|
||||
* @property {object=} options
|
||||
* @property {Plugin[]} [plugins]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @interface GetConfig
|
||||
* @description Rendering configuration for rendering an existing object
|
||||
* @extends BaseConfig
|
||||
* @property {string} id
|
||||
*/
|
||||
|
||||
export default async function getObject({ id, options, plugins, element }, halo) {
|
||||
const key = `${id}`;
|
||||
let rpc = rpcRequestModelStore.get(key);
|
||||
if (!rpc) {
|
||||
rpc = halo.app.getObject(id);
|
||||
rpcRequestModelStore.set(key, rpc);
|
||||
}
|
||||
const model = await rpc;
|
||||
modelStore.set(key, model);
|
||||
|
||||
if (model.genericType === 'sheet') {
|
||||
return initSheet(model, { options, plugins, element }, halo);
|
||||
}
|
||||
|
||||
return init(model, { options, plugins, element }, halo);
|
||||
}
|
||||
@@ -1,21 +1,6 @@
|
||||
import init from './initiate';
|
||||
import { modelStore, rpcRequestModelStore } from '../stores/model-store';
|
||||
|
||||
/**
|
||||
* @interface BaseConfig
|
||||
* @description Basic rendering configuration for rendering an object
|
||||
* @property {HTMLElement} element
|
||||
* @property {object=} options
|
||||
* @property {Plugin[]} [plugins]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @interface GetConfig
|
||||
* @description Rendering configuration for rendering an existing object
|
||||
* @extends BaseConfig
|
||||
* @property {string} id
|
||||
*/
|
||||
|
||||
export default async function getObject({ id, options, plugins, element }, halo) {
|
||||
const key = `${id}`;
|
||||
let rpc = rpcRequestModelStore.get(key);
|
||||
@@ -25,5 +10,6 @@ export default async function getObject({ id, options, plugins, element }, halo)
|
||||
}
|
||||
const model = await rpc;
|
||||
modelStore.set(key, model);
|
||||
|
||||
return init(model, { options, plugins, element }, halo);
|
||||
}
|
||||
|
||||
23
apis/nucleus/src/object/initiate-sheet.js
Normal file
23
apis/nucleus/src/object/initiate-sheet.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/* eslint no-underscore-dangle:0 */
|
||||
import sheetAPI from '../sheet';
|
||||
|
||||
export default async function initSheet(model, optional, halo, initialError, onDestroy = async () => {}) {
|
||||
const api = sheetAPI({
|
||||
model,
|
||||
halo,
|
||||
initialError,
|
||||
onDestroy,
|
||||
});
|
||||
|
||||
if (optional.options) {
|
||||
api.__DO_NOT_USE__.options(optional.options);
|
||||
}
|
||||
if (optional.plugins) {
|
||||
api.__DO_NOT_USE__.plugins(optional.plugins);
|
||||
}
|
||||
if (optional.element) {
|
||||
await api.__DO_NOT_USE__.mount(optional.element);
|
||||
}
|
||||
|
||||
return api;
|
||||
}
|
||||
134
apis/nucleus/src/sheet.js
Normal file
134
apis/nucleus/src/sheet.js
Normal file
@@ -0,0 +1,134 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
import glueSheet from './components/sheetGlue';
|
||||
import validatePlugins from './plugins/plugins';
|
||||
import getPatches from './utils/patcher';
|
||||
|
||||
const noopi = () => {};
|
||||
|
||||
export default function sheet({ model, halo, initialError, onDestroy = async () => {} } = {}) {
|
||||
let unmountSheet = noopi;
|
||||
let sheetRef = null;
|
||||
let mountedReference = null;
|
||||
let onMount = null;
|
||||
const mounted = new Promise((resolve) => {
|
||||
onMount = resolve;
|
||||
});
|
||||
|
||||
let initialSnOptions = {};
|
||||
let initialSnPlugins = [];
|
||||
|
||||
const setSnOptions = async (opts) => {
|
||||
if (mountedReference) {
|
||||
(async () => {
|
||||
await mounted;
|
||||
sheetRef.current.setSnOptions({
|
||||
...initialSnOptions,
|
||||
...opts,
|
||||
});
|
||||
})();
|
||||
} else {
|
||||
// Handle setting options before mount
|
||||
initialSnOptions = {
|
||||
...initialSnOptions,
|
||||
...opts,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const setSnPlugins = async (plugins) => {
|
||||
validatePlugins(plugins);
|
||||
if (mountedReference) {
|
||||
(async () => {
|
||||
await mounted;
|
||||
sheetRef.current.setSnPlugins(plugins);
|
||||
})();
|
||||
} else {
|
||||
// Handle setting plugins before mount
|
||||
initialSnPlugins = plugins;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @class
|
||||
* @alias Sheet
|
||||
* @classdesc A controller to further modify a visualization after it has been rendered.
|
||||
* @experimental
|
||||
* @since 3.1.0
|
||||
* @example
|
||||
* const sheet = await embed(app).render({
|
||||
* element,
|
||||
* id: "jD5Gd"
|
||||
* });
|
||||
* sheet.destroy();
|
||||
*/
|
||||
const api = /** @lends Sheet# */ {
|
||||
/**
|
||||
* The id of this sheets's generic object.
|
||||
* @type {string}
|
||||
*/
|
||||
id: model.id,
|
||||
/**
|
||||
* This sheets Enigma model, a representation of the generic object.
|
||||
* @type {string}
|
||||
*/
|
||||
model,
|
||||
/**
|
||||
* Destroys the sheet and removes it from the the DOM.
|
||||
* @example
|
||||
* const sheet = await embed(app).render({
|
||||
* element,
|
||||
* id: "jD5Gd"
|
||||
* });
|
||||
* sheet.destroy();
|
||||
*/
|
||||
async destroy() {
|
||||
await onDestroy();
|
||||
unmountSheet();
|
||||
unmountSheet = noopi;
|
||||
},
|
||||
// ===== unexposed experimental API - use at own risk ======
|
||||
__DO_NOT_USE__: {
|
||||
mount(element) {
|
||||
if (mountedReference) {
|
||||
throw new Error('Already mounted');
|
||||
}
|
||||
mountedReference = element;
|
||||
[unmountSheet, sheetRef] = glueSheet({
|
||||
halo,
|
||||
element,
|
||||
model,
|
||||
initialSnOptions,
|
||||
initialSnPlugins,
|
||||
initialError,
|
||||
onMount,
|
||||
});
|
||||
return mounted;
|
||||
},
|
||||
async applyProperties(props) {
|
||||
const current = await model.getEffectiveProperties();
|
||||
const patches = getPatches('/', props, current);
|
||||
if (patches.length) {
|
||||
return model.applyPatches(patches, true);
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
options(opts) {
|
||||
setSnOptions(opts);
|
||||
},
|
||||
plugins(plugins) {
|
||||
setSnPlugins(plugins);
|
||||
},
|
||||
exportImage() {
|
||||
throw new Error('Not implemented');
|
||||
},
|
||||
takeSnapshot() {
|
||||
throw new Error('Not implemented');
|
||||
},
|
||||
getModel() {
|
||||
return model;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return api;
|
||||
}
|
||||
@@ -62,7 +62,7 @@ export const resolveBgImage = (bgComp, app) => {
|
||||
|
||||
if (bgImageDef) {
|
||||
let url = '';
|
||||
if (bgImageDef.mode === 'media') {
|
||||
if (bgImageDef.mode === 'media' || bgComp.useImage === 'media') {
|
||||
url = bgImageDef?.mediaUrl?.qStaticContentUrl?.qUrl
|
||||
? decodeURIComponent(bgImageDef.mediaUrl.qStaticContentUrl.qUrl)
|
||||
: undefined;
|
||||
@@ -85,7 +85,7 @@ export const resolveBgColor = (bgComp, theme) => {
|
||||
if (bgColor.useColorExpression) {
|
||||
return theme.validateColor(bgColor.colorExpression);
|
||||
}
|
||||
return bgColor.color && bgColor.color.color !== 'none' ? theme.getColorPickerColor(bgColor.color) : undefined;
|
||||
return bgColor.color && bgColor.color.color !== 'none' ? theme.getColorPickerColor(bgColor.color, true) : undefined;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
@@ -66,6 +66,11 @@ export default function viz({ model, halo, initialError, onDestroy = async () =>
|
||||
* @type {string}
|
||||
*/
|
||||
id: model.id,
|
||||
/**
|
||||
* This visualizations Enigma model, a representation of the generic object.
|
||||
* @type {string}
|
||||
*/
|
||||
model,
|
||||
/**
|
||||
* Destroys the visualization and removes it from the the DOM.
|
||||
* @example
|
||||
|
||||
@@ -648,7 +648,7 @@
|
||||
},
|
||||
"entries": {
|
||||
"render": {
|
||||
"description": "Renders a visualization into an HTMLElement.",
|
||||
"description": "Renders a visualization or sheet into an HTMLElement.\nSupport for sense sheets is experimental.",
|
||||
"kind": "function",
|
||||
"params": [
|
||||
{
|
||||
@@ -666,11 +666,19 @@
|
||||
}
|
||||
],
|
||||
"returns": {
|
||||
"description": "A controller to the rendered visualization.",
|
||||
"description": "A controller to the rendered visualization or sheet.",
|
||||
"type": "Promise",
|
||||
"generics": [
|
||||
{
|
||||
"type": "#/definitions/Viz"
|
||||
"kind": "union",
|
||||
"items": [
|
||||
{
|
||||
"type": "#/definitions/Viz"
|
||||
},
|
||||
{
|
||||
"type": "#/definitions/Sheet"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -991,6 +999,39 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Sheet": {
|
||||
"description": "A controller to further modify a visualization after it has been rendered.",
|
||||
"stability": "experimental",
|
||||
"availability": {
|
||||
"since": "3.1.0"
|
||||
},
|
||||
"kind": "class",
|
||||
"constructor": {
|
||||
"kind": "function",
|
||||
"params": []
|
||||
},
|
||||
"entries": {
|
||||
"id": {
|
||||
"description": "The id of this sheets's generic object.",
|
||||
"type": "string"
|
||||
},
|
||||
"model": {
|
||||
"description": "This sheets Enigma model, a representation of the generic object.",
|
||||
"type": "string"
|
||||
},
|
||||
"destroy": {
|
||||
"description": "Destroys the sheet and removes it from the the DOM.",
|
||||
"kind": "function",
|
||||
"params": [],
|
||||
"examples": [
|
||||
"const sheet = await embed(app).render({\n element,\n id: \"jD5Gd\"\n});\nsheet.destroy();"
|
||||
]
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
"const sheet = await embed(app).render({\n element,\n id: \"jD5Gd\"\n});\nsheet.destroy();"
|
||||
]
|
||||
},
|
||||
"Viz": {
|
||||
"description": "A controller to further modify a visualization after it has been rendered.",
|
||||
"kind": "class",
|
||||
@@ -1003,6 +1044,10 @@
|
||||
"description": "The id of this visualization's generic object.",
|
||||
"type": "string"
|
||||
},
|
||||
"model": {
|
||||
"description": "This visualizations Enigma model, a representation of the generic object.",
|
||||
"type": "string"
|
||||
},
|
||||
"destroy": {
|
||||
"description": "Destroys the visualization and removes it from the the DOM.",
|
||||
"kind": "function",
|
||||
@@ -2117,6 +2162,12 @@
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "supportNone",
|
||||
"description": "Shifts the palette index by one to account for the \"none\" color",
|
||||
"optional": true,
|
||||
"type": "boolean"
|
||||
}
|
||||
],
|
||||
"returns": {
|
||||
@@ -2124,7 +2175,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"examples": [
|
||||
"theme.getColorPickerColor({ index: 1 });\ntheme.getColorPickerColor({ color: 'red' });"
|
||||
"theme.getColorPickerColor({ index: 1 });\ntheme.getColorPickerColor({ index: 1 }, true);\ntheme.getColorPickerColor({ color: 'red' });"
|
||||
]
|
||||
},
|
||||
"getContrastingColorTo": {
|
||||
|
||||
27
apis/stardust/types/index.d.ts
vendored
27
apis/stardust/types/index.d.ts
vendored
@@ -213,10 +213,11 @@ declare namespace stardust {
|
||||
constructor();
|
||||
|
||||
/**
|
||||
* Renders a visualization into an HTMLElement.
|
||||
* Renders a visualization or sheet into an HTMLElement.
|
||||
* Support for sense sheets is experimental.
|
||||
* @param cfg The render configuration.
|
||||
*/
|
||||
render(cfg: stardust.CreateConfig | stardust.GetConfig): Promise<stardust.Viz>;
|
||||
render(cfg: stardust.CreateConfig | stardust.GetConfig): Promise<stardust.Viz | stardust.Sheet>;
|
||||
|
||||
/**
|
||||
* Updates the current context of this embed instance.
|
||||
@@ -294,6 +295,23 @@ declare namespace stardust {
|
||||
qId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A controller to further modify a visualization after it has been rendered.
|
||||
*/
|
||||
class Sheet {
|
||||
constructor();
|
||||
|
||||
id: string;
|
||||
|
||||
model: string;
|
||||
|
||||
/**
|
||||
* Destroys the sheet and removes it from the the DOM.
|
||||
*/
|
||||
destroy(): void;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A controller to further modify a visualization after it has been rendered.
|
||||
*/
|
||||
@@ -302,6 +320,8 @@ declare namespace stardust {
|
||||
|
||||
id: string;
|
||||
|
||||
model: string;
|
||||
|
||||
/**
|
||||
* Destroys the visualization and removes it from the the DOM.
|
||||
*/
|
||||
@@ -618,11 +638,12 @@ declare namespace stardust {
|
||||
/**
|
||||
* Resolve a color object using the color picker palette from the provided JSON theme.
|
||||
* @param c
|
||||
* @param supportNone Shifts the palette index by one to account for the "none" color
|
||||
*/
|
||||
getColorPickerColor(c: {
|
||||
index?: number;
|
||||
color?: string;
|
||||
}): string;
|
||||
}, supportNone?: boolean): string;
|
||||
|
||||
/**
|
||||
* Get the best contrasting color against the specified `color`.
|
||||
|
||||
@@ -49,10 +49,12 @@ export default function theme() {
|
||||
* @param {object} c
|
||||
* @param {number=} c.index
|
||||
* @param {string=} c.color
|
||||
* @param {boolean=} supportNone Shifts the palette index by one to account for the "none" color
|
||||
* @returns {string} The resolved color.
|
||||
*
|
||||
* @example
|
||||
* theme.getColorPickerColor({ index: 1 });
|
||||
* theme.getColorPickerColor({ index: 1 }, true);
|
||||
* theme.getColorPickerColor({ color: 'red' });
|
||||
*/
|
||||
getColorPickerColor(...a) {
|
||||
|
||||
@@ -75,7 +75,9 @@ export default function theme(resolvedTheme) {
|
||||
others: resolvedTheme.dataColors.othersColor,
|
||||
};
|
||||
},
|
||||
uiColor(c) {
|
||||
uiColor(c, shift) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
shift = !!shift;
|
||||
if (c.index < 0 || typeof c.index === 'undefined') {
|
||||
return c.color;
|
||||
}
|
||||
@@ -85,10 +87,10 @@ export default function theme(resolvedTheme) {
|
||||
if (!uiPalette) {
|
||||
return c.color;
|
||||
}
|
||||
if (typeof uiPalette.colors[c.index] === 'undefined') {
|
||||
if (typeof uiPalette.colors[c.index - shift] === 'undefined') {
|
||||
return c.color;
|
||||
}
|
||||
return uiPalette.colors[c.index];
|
||||
return uiPalette.colors[c.index - shift];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
const cmd = 'mocha test/rendering/listbox/listbox.spec.js --bail false --timeout 30000';
|
||||
const cmd = 'mocha test/rendering/**/*.spec.js --bail false --timeout 30000';
|
||||
execSync(cmd, { stdio: 'inherit' });
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgb(50, 50, 50);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
BIN
test/rendering/__artifacts__/baseline/sheet_basic.png
Normal file
BIN
test/rendering/__artifacts__/baseline/sheet_basic.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.7 KiB |
45
test/rendering/sheet/configured.js
Normal file
45
test/rendering/sheet/configured.js
Normal file
@@ -0,0 +1,45 @@
|
||||
const pie = {
|
||||
component: {
|
||||
mounted(el) {
|
||||
// eslint-disable-next-line
|
||||
el.innerHTML = '<div id="pie" style="background: aliceblue; height:100%; width:100%;">Hello pie</div>';
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const bar = function (env) {
|
||||
env.translator.add({
|
||||
id: 'hello',
|
||||
locale: {
|
||||
'sv-SE': 'Hej {0}!',
|
||||
},
|
||||
});
|
||||
return {
|
||||
component: {
|
||||
mounted(el) {
|
||||
// eslint-disable-next-line
|
||||
el.innerHTML = `<div id="bar" style="font-size: 64px; background: tan; height:100%; width:100%;">${env.translator.get(
|
||||
'hello',
|
||||
['bar']
|
||||
)}</div>`;
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// eslint-disable-next-line
|
||||
const configured = stardust.embed.createConfiguration({
|
||||
context: {
|
||||
language: 'sv-SE',
|
||||
},
|
||||
types: [
|
||||
{
|
||||
name: 'piechart',
|
||||
load: () => Promise.resolve(pie),
|
||||
},
|
||||
{
|
||||
name: 'barchart',
|
||||
load: () => Promise.resolve(bar),
|
||||
},
|
||||
],
|
||||
});
|
||||
50
test/rendering/sheet/sheet-data.js
Normal file
50
test/rendering/sheet/sheet-data.js
Normal file
@@ -0,0 +1,50 @@
|
||||
/* eslint arrow-body-style: 0 */
|
||||
|
||||
window.getFuncs = function getFuncs() {
|
||||
return {
|
||||
getSheetLayout: () => {
|
||||
return {
|
||||
qInfo: {
|
||||
qId: 'sheet',
|
||||
},
|
||||
visualization: 'sheet',
|
||||
cells: [
|
||||
{
|
||||
name: 'bar',
|
||||
bounds: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 50,
|
||||
height: 50,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'pie',
|
||||
bounds: {
|
||||
x: 50,
|
||||
y: 50,
|
||||
width: 50,
|
||||
height: 50,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
getBarLayout: () => {
|
||||
return {
|
||||
qInfo: {
|
||||
qId: 'bar',
|
||||
},
|
||||
visualization: 'barchart',
|
||||
};
|
||||
},
|
||||
getPieLayout: () => {
|
||||
return {
|
||||
qInfo: {
|
||||
qId: 'pie',
|
||||
},
|
||||
visualization: 'piechart',
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
28
test/rendering/sheet/sheet.html
Normal file
28
test/rendering/sheet/sheet.html
Normal file
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script src="/apis/stardust/dist/stardust.js"></script>
|
||||
<script src="configured.js"></script>
|
||||
<script src="sheet-data.js"></script>
|
||||
<script defer src="sheet.js"></script>
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#object {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="object" data-type="sheet"></div>
|
||||
</body>
|
||||
</html>
|
||||
46
test/rendering/sheet/sheet.js
Normal file
46
test/rendering/sheet/sheet.js
Normal file
@@ -0,0 +1,46 @@
|
||||
/* global configured */
|
||||
/* eslint no-underscore-dangle: 0 */
|
||||
(() => {
|
||||
async function getMocks(EnigmaMocker) {
|
||||
const { getSheetLayout, getBarLayout, getPieLayout } = window.getFuncs();
|
||||
|
||||
const obj = [
|
||||
{
|
||||
id: `sheet`,
|
||||
type: 'sheet',
|
||||
getLayout: () => getSheetLayout(),
|
||||
},
|
||||
{
|
||||
id: `bar`,
|
||||
type: 'barchart',
|
||||
getLayout: () => getBarLayout(),
|
||||
},
|
||||
{
|
||||
id: `pie`,
|
||||
type: 'piechart',
|
||||
getLayout: () => getPieLayout(),
|
||||
},
|
||||
];
|
||||
|
||||
const app = await EnigmaMocker.fromGenericObjects(obj);
|
||||
|
||||
return {
|
||||
obj,
|
||||
app,
|
||||
};
|
||||
}
|
||||
|
||||
const init = async () => {
|
||||
const element = document.querySelector('#object');
|
||||
const { app } = await getMocks(window.stardust.EnigmaMocker);
|
||||
|
||||
const nebbie = configured(app);
|
||||
|
||||
const inst = await nebbie.render({ id: 'sheet', element });
|
||||
return () => {
|
||||
inst?.unmount(element);
|
||||
};
|
||||
};
|
||||
|
||||
return init();
|
||||
})();
|
||||
36
test/rendering/sheet/sheet.spec.js
Normal file
36
test/rendering/sheet/sheet.spec.js
Normal file
@@ -0,0 +1,36 @@
|
||||
const getPage = require('../setup');
|
||||
const startServer = require('../server');
|
||||
const { looksLike } = require('../testUtils');
|
||||
|
||||
describe('listbox mashup rendering test', () => {
|
||||
const object = '[data-type="sheet"]';
|
||||
let page;
|
||||
let takeScreenshot;
|
||||
let destroyServer;
|
||||
let destroyBrowser;
|
||||
|
||||
let url;
|
||||
const PAGE_OPTIONS = { width: 600, height: 500 };
|
||||
|
||||
beforeEach(async () => {
|
||||
({ url, destroy: destroyServer } = await startServer());
|
||||
({ page, takeScreenshot, destroy: destroyBrowser } = await getPage(PAGE_OPTIONS));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all([destroyServer(), destroyBrowser()]);
|
||||
});
|
||||
|
||||
it('selecting two values should result in two green rows', async () => {
|
||||
const FILE_NAME = 'sheet_basic.png';
|
||||
|
||||
await page.goto(`${url}/sheet/sheet.html`);
|
||||
await page.waitForSelector(object, { visible: true });
|
||||
|
||||
const snapshotElement = await page.$(object);
|
||||
await page.$('#bar');
|
||||
await page.$('#pie');
|
||||
const { path: capturedPath } = await takeScreenshot(FILE_NAME, snapshotElement);
|
||||
await looksLike(FILE_NAME, capturedPath);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user