diff --git a/.circleci/config.yml b/.circleci/config.yml
index 8bfbff86d..cdb4ace11 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -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'
diff --git a/.gitignore b/.gitignore
index 794b7ead1..6ad681ab9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
*.rej
*.tmp
*.log
+*.pem
.cache/
.DS_Store
.idea/
diff --git a/apis/enigma-mocker/src/mocks/get-object-mock.js b/apis/enigma-mocker/src/mocks/get-object-mock.js
index cad65eccc..d6352818f 100644
--- a/apis/enigma-mocker/src/mocks/get-object-mock.js
+++ b/apis/enigma-mocker/src/mocks/get-object-mock.js
@@ -67,6 +67,7 @@ function createMock(genericObject, options) {
}),
{}
),
+ genericType: genericObject.type,
};
return { [qId]: mock };
}
diff --git a/apis/nucleus/src/components/Cell.jsx b/apis/nucleus/src/components/Cell.jsx
index 7523a5500..670edde61 100644
--- a/apis/nucleus/src/components/Cell.jsx
+++ b/apis/nucleus/src/components/Cell.jsx
@@ -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,
diff --git a/apis/nucleus/src/components/NebulaApp.jsx b/apis/nucleus/src/components/NebulaApp.jsx
index 134b3c2bc..e7fa22b51 100644
--- a/apis/nucleus/src/components/NebulaApp.jsx
+++ b/apis/nucleus/src/components/NebulaApp.jsx
@@ -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;
diff --git a/apis/nucleus/src/components/Sheet.jsx b/apis/nucleus/src/components/Sheet.jsx
new file mode 100644
index 000000000..673c7ac69
--- /dev/null
+++ b/apis/nucleus/src/components/Sheet.jsx
@@ -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 (
+
+ |
+
+ );
+}
+
+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 (
+
+ {cellRenderers}
+
+ );
+}
+
+export default Sheet;
diff --git a/apis/nucleus/src/components/sheetGlue.jsx b/apis/nucleus/src/components/sheetGlue.jsx
new file mode 100644
index 000000000..ce9ae0543
--- /dev/null
+++ b/apis/nucleus/src/components/sheetGlue.jsx
@@ -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(
+ ,
+ element,
+ model.id
+ );
+
+ const unmount = () => {
+ root.remove(portal);
+ model.removeListener('closed', unmount);
+ };
+
+ model.on('closed', unmount);
+
+ root.add(portal);
+ return [unmount, sheetRef];
+}
diff --git a/apis/nucleus/src/index.js b/apis/nucleus/src/index.js
index 8ca7905ae..ae947b43d 100644
--- a/apis/nucleus/src/index.js
+++ b/apis/nucleus/src/index.js
@@ -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} A controller to the rendered visualization.
+ * @returns {Promise} A controller to the rendered visualization or sheet.
* @example
* // render from existing object
* n.render({
diff --git a/apis/nucleus/src/object/get-generic-object.js b/apis/nucleus/src/object/get-generic-object.js
new file mode 100644
index 000000000..17e4b9390
--- /dev/null
+++ b/apis/nucleus/src/object/get-generic-object.js
@@ -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);
+}
diff --git a/apis/nucleus/src/object/get-object.js b/apis/nucleus/src/object/get-object.js
index d9e9f172c..f5cb486fa 100644
--- a/apis/nucleus/src/object/get-object.js
+++ b/apis/nucleus/src/object/get-object.js
@@ -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);
}
diff --git a/apis/nucleus/src/object/initiate-sheet.js b/apis/nucleus/src/object/initiate-sheet.js
new file mode 100644
index 000000000..dbfc6f03c
--- /dev/null
+++ b/apis/nucleus/src/object/initiate-sheet.js
@@ -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;
+}
diff --git a/apis/nucleus/src/sheet.js b/apis/nucleus/src/sheet.js
new file mode 100644
index 000000000..0f6ef7818
--- /dev/null
+++ b/apis/nucleus/src/sheet.js
@@ -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;
+}
diff --git a/apis/nucleus/src/utils/background-props.js b/apis/nucleus/src/utils/background-props.js
index 57b0bf25e..30127b8c7 100644
--- a/apis/nucleus/src/utils/background-props.js
+++ b/apis/nucleus/src/utils/background-props.js
@@ -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;
};
diff --git a/apis/nucleus/src/viz.js b/apis/nucleus/src/viz.js
index 14e7f4023..b7949e562 100644
--- a/apis/nucleus/src/viz.js
+++ b/apis/nucleus/src/viz.js
@@ -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
diff --git a/apis/stardust/api-spec/spec.json b/apis/stardust/api-spec/spec.json
index 4e3413035..ed8b0c1ac 100644
--- a/apis/stardust/api-spec/spec.json
+++ b/apis/stardust/api-spec/spec.json
@@ -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": {
diff --git a/apis/stardust/types/index.d.ts b/apis/stardust/types/index.d.ts
index 9ffc2348f..cb015ea21 100644
--- a/apis/stardust/types/index.d.ts
+++ b/apis/stardust/types/index.d.ts
@@ -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;
+ render(cfg: stardust.CreateConfig | stardust.GetConfig): Promise;
/**
* 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`.
diff --git a/apis/theme/src/index.js b/apis/theme/src/index.js
index 8085126cb..c5362b2d4 100644
--- a/apis/theme/src/index.js
+++ b/apis/theme/src/index.js
@@ -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) {
diff --git a/apis/theme/src/palette-resolver.js b/apis/theme/src/palette-resolver.js
index 0e5fe632c..26fb7c9ab 100644
--- a/apis/theme/src/palette-resolver.js
+++ b/apis/theme/src/palette-resolver.js
@@ -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];
},
};
}
diff --git a/scripts/run-rendering-tests.js b/scripts/run-rendering-tests.js
index 919c40811..ac32bbcae 100644
--- a/scripts/run-rendering-tests.js
+++ b/scripts/run-rendering-tests.js
@@ -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' });
diff --git a/test/mashup/snaps/single.html b/test/mashup/snaps/single.html
index f89d14894..7bc23bbdc 100644
--- a/test/mashup/snaps/single.html
+++ b/test/mashup/snaps/single.html
@@ -18,6 +18,7 @@
position: absolute;
width: 100%;
height: 100%;
+ background-color: rgb(50, 50, 50);
}
diff --git a/test/rendering/__artifacts__/baseline/sheet_basic.png b/test/rendering/__artifacts__/baseline/sheet_basic.png
new file mode 100644
index 000000000..2d87bae97
Binary files /dev/null and b/test/rendering/__artifacts__/baseline/sheet_basic.png differ
diff --git a/test/rendering/sheet/configured.js b/test/rendering/sheet/configured.js
new file mode 100644
index 000000000..6d97db151
--- /dev/null
+++ b/test/rendering/sheet/configured.js
@@ -0,0 +1,45 @@
+const pie = {
+ component: {
+ mounted(el) {
+ // eslint-disable-next-line
+ el.innerHTML = 'Hello pie
';
+ },
+ },
+};
+
+const bar = function (env) {
+ env.translator.add({
+ id: 'hello',
+ locale: {
+ 'sv-SE': 'Hej {0}!',
+ },
+ });
+ return {
+ component: {
+ mounted(el) {
+ // eslint-disable-next-line
+ el.innerHTML = `${env.translator.get(
+ 'hello',
+ ['bar']
+ )}
`;
+ },
+ },
+ };
+};
+
+// 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),
+ },
+ ],
+});
diff --git a/test/rendering/sheet/sheet-data.js b/test/rendering/sheet/sheet-data.js
new file mode 100644
index 000000000..0eae89c61
--- /dev/null
+++ b/test/rendering/sheet/sheet-data.js
@@ -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',
+ };
+ },
+ };
+};
diff --git a/test/rendering/sheet/sheet.html b/test/rendering/sheet/sheet.html
new file mode 100644
index 000000000..695985351
--- /dev/null
+++ b/test/rendering/sheet/sheet.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/rendering/sheet/sheet.js b/test/rendering/sheet/sheet.js
new file mode 100644
index 000000000..4b3742a0c
--- /dev/null
+++ b/test/rendering/sheet/sheet.js
@@ -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();
+})();
diff --git a/test/rendering/sheet/sheet.spec.js b/test/rendering/sheet/sheet.spec.js
new file mode 100644
index 000000000..f3454aaa0
--- /dev/null
+++ b/test/rendering/sheet/sheet.spec.js
@@ -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);
+ });
+});