diff --git a/apis/nucleus/src/components/Cell.jsx b/apis/nucleus/src/components/Cell.jsx
index 5173436cf..45b7703c6 100644
--- a/apis/nucleus/src/components/Cell.jsx
+++ b/apis/nucleus/src/components/Cell.jsx
@@ -129,7 +129,6 @@ const validateInfo = (min, info, getDescription, translatedError, translatedCalc
}`;
const customDescription = getDescription(i);
const description = customDescription ? `${customDescription}${label.length ? delimiter : ''}` : null;
-
return {
description,
label,
@@ -139,21 +138,22 @@ const validateInfo = (min, info, getDescription, translatedError, translatedCalc
});
};
+const getInfo = (info) => (info && (Array.isArray(info) ? info : [info])) || [];
+
const validateTarget = (translator, layout, properties, def) => {
const minD = def.dimensions.min();
const minM = def.measures.min();
- const hc = def.resolveLayout(layout);
-
+ const c = def.resolveLayout(layout);
const reqDimErrors = validateInfo(
minD,
- hc.qDimensionInfo,
+ getInfo(c.qDimensionInfo),
(i) => def.dimensions.description(properties, i),
translator.get('Visualization.Invalid.Dimension'),
translator.get('Visualization.UnfulfilledCalculationCondition')
);
const reqMeasErrors = validateInfo(
minM,
- hc.qMeasureInfo,
+ getInfo(c.qMeasureInfo),
(i) => def.measures.description(properties, i),
translator.get('Visualization.Invalid.Measure'),
translator.get('Visualization.UnfulfilledCalculationCondition')
@@ -175,21 +175,21 @@ const validateCubes = (translator, targets, layout) => {
const def = targets[i];
const minD = def.dimensions.min();
const minM = def.measures.min();
- const hc = def.resolveLayout(layout);
- const d = (hc.qDimensionInfo || []).filter(filterData); // Filter out optional calc conditions
- const m = (hc.qMeasureInfo || []).filter(filterData); // Filter out optional calc conditions
+ const c = def.resolveLayout(layout);
+ const d = getInfo(c.qDimensionInfo).filter(filterData); // Filter out optional calc conditions
+ const m = getInfo(c.qMeasureInfo).filter(filterData); // Filter out optional calc conditions
aggMinD += minD;
aggMinM += minM;
if (d.length < minD || m.length < minM) {
hasUnfulfilledErrors = true;
}
- if (hc.qError) {
+ if (c.qError) {
hasLayoutErrors = true;
- hasLayoutUnfulfilledCalculcationCondition = hc.qError.qErrorCode === 7005;
+ hasLayoutUnfulfilledCalculcationCondition = c.qError.qErrorCode === 7005;
const title =
// eslint-disable-next-line no-nested-ternary
- hasLayoutUnfulfilledCalculcationCondition && hc.qCalcCondMsg
- ? hc.qCalcCondMsg
+ hasLayoutUnfulfilledCalculcationCondition && c.qCalcCondMsg
+ ? c.qCalcCondMsg
: hasLayoutUnfulfilledCalculcationCondition
? translator.get('Visualization.UnfulfilledCalculationCondition')
: translator.get('Visualization.LayoutError');
diff --git a/apis/nucleus/src/object/__tests__/hc-handler.spec.js b/apis/nucleus/src/object/__tests__/hc-handler.spec.js
index f2a3afdb9..92594c172 100644
--- a/apis/nucleus/src/object/__tests__/hc-handler.spec.js
+++ b/apis/nucleus/src/object/__tests__/hc-handler.spec.js
@@ -25,7 +25,7 @@ describe('hc-handler', () => {
};
h = handler({
- hc,
+ dc: hc,
def,
properties: 'props',
});
diff --git a/apis/nucleus/src/object/__tests__/populator.spec.js b/apis/nucleus/src/object/__tests__/populator.spec.js
index a9fb8c5a4..10c2b4e5f 100644
--- a/apis/nucleus/src/object/__tests__/populator.spec.js
+++ b/apis/nucleus/src/object/__tests__/populator.spec.js
@@ -56,7 +56,7 @@ describe('populator', () => {
const resolved = { qDimensions: [] };
populate({ sn, properties: { a: { b: { c: resolved } } }, fields: [1] });
expect(handler).to.have.been.calledWithExactly({
- hc: resolved,
+ dc: resolved,
def: target,
properties: { a: { b: { c: resolved } } },
});
diff --git a/apis/nucleus/src/object/hc-handler.js b/apis/nucleus/src/object/hc-handler.js
index d82041011..e6d5cd1ca 100644
--- a/apis/nucleus/src/object/hc-handler.js
+++ b/apis/nucleus/src/object/hc-handler.js
@@ -35,7 +35,7 @@ const nxMeasure = (f) => ({
},
});
-export default function hcHandler({ hc, def, properties }) {
+export default function hcHandler({ dc: hc, def, properties }) {
hc.qDimensions = hc.qDimensions || [];
hc.qMeasures = hc.qMeasures || [];
hc.qInterColumnSortOrder = hc.qInterColumnSortOrder || [];
@@ -45,7 +45,7 @@ export default function hcHandler({ hc, def, properties }) {
const objectProperties = properties;
- const h = {
+ const handler = {
dimensions() {
return hc.qDimensions;
},
@@ -78,7 +78,7 @@ export default function hcHandler({ hc, def, properties }) {
dimension.qAttributeDimensions = dimension.qAttributeDimensions || [];
// ========= end defaults =============
- if (hc.qDimensions.length < h.maxDimensions()) {
+ if (hc.qDimensions.length < handler.maxDimensions()) {
hc.qDimensions.push(dimension);
addIndex(hc.qInterColumnSortOrder, hc.qDimensions.length - 1);
def.dimensions.added(dimension, objectProperties);
@@ -115,7 +115,7 @@ export default function hcHandler({ hc, def, properties }) {
measure.qAttributeDimensions = measure.qAttributeDimensions || [];
measure.qAttributeExpressions = measure.qAttributeExpressions || [];
- if (hc.qMeasures.length < h.maxMeasures()) {
+ if (hc.qMeasures.length < handler.maxMeasures()) {
hc.qMeasures.push(measure);
addIndex(hc.qInterColumnSortOrder, hc.qDimensions.length + hc.qMeasures.length - 1);
def.measures.added(measure, objectProperties);
@@ -141,12 +141,12 @@ export default function hcHandler({ hc, def, properties }) {
return def.measures.max(hc.qDimensions.length);
},
canAddDimension() {
- return hc.qDimensions.length < h.maxDimensions();
+ return hc.qDimensions.length < handler.maxDimensions();
},
canAddMeasure() {
- return hc.qMeasures.length < h.maxMeasures();
+ return hc.qMeasures.length < handler.maxMeasures();
},
};
- return h;
+ return handler;
}
diff --git a/apis/nucleus/src/object/lo-handler.js b/apis/nucleus/src/object/lo-handler.js
new file mode 100644
index 000000000..0e716324f
--- /dev/null
+++ b/apis/nucleus/src/object/lo-handler.js
@@ -0,0 +1,72 @@
+/* eslint no-param-reassign:0 */
+
+import uid from './uid';
+
+const nxDimension = (f) => ({
+ qDef: {
+ qFieldDefs: [f],
+ },
+});
+
+export default function loHandler({ dc: lo, def, properties }) {
+ lo.qInitialDataFetch = lo.qInitialDataFetch || [];
+
+ const objectProperties = properties;
+
+ const handler = {
+ dimensions() {
+ if (!lo.qDef || !lo.qDef.qFieldDefs || lo.qDef.qFieldDefs.length === 0) return [];
+ return [lo];
+ },
+ measures() {
+ return [];
+ },
+ addDimension(d) {
+ const dimension =
+ typeof d === 'string'
+ ? nxDimension(d)
+ : {
+ ...d,
+ qDef: d.qDef || {},
+ };
+ dimension.qDef.cId = dimension.qDef.cId || uid();
+
+ dimension.qDef.qSortCriterias = dimension.qDef.qSortCriterias || [
+ {
+ qSortByState: 1,
+ qSortByLoadOrder: 1,
+ qSortByNumeric: 1,
+ qSortByAscii: 1,
+ },
+ ];
+ Object.keys(dimension).forEach((k) => {
+ lo[k] = dimension[k];
+ });
+ def.dimensions.added(dimension, objectProperties);
+ },
+ removeDimension(idx) {
+ const dimension = lo;
+ Object.keys(dimension).forEach((k) => {
+ delete lo[k];
+ });
+ def.dimensions.removed(dimension, objectProperties, idx);
+ },
+ addMeasure() {},
+ removeMeasure() {},
+
+ maxDimensions() {
+ return 1;
+ },
+ maxMeasures() {
+ return 0;
+ },
+ canAddDimension() {
+ return lo.qDef && lo.qDef.qFieldDefs ? lo.qDef.qFieldDefs.length === 0 : !lo.qDef;
+ },
+ canAddMeasure() {
+ return false;
+ },
+ };
+
+ return handler;
+}
diff --git a/apis/nucleus/src/object/populator.js b/apis/nucleus/src/object/populator.js
index 65a37939e..fd9d0e225 100644
--- a/apis/nucleus/src/object/populator.js
+++ b/apis/nucleus/src/object/populator.js
@@ -45,7 +45,7 @@ export default function populateData({ sn, properties, fields }) {
}
const hc = hcHandler({
- hc: p,
+ dc: p,
def: target,
properties,
});
diff --git a/apis/supernova/__tests__/unit/qae.spec.js b/apis/supernova/__tests__/unit/qae.spec.js
index 4d24cd6e4..c92306fdf 100644
--- a/apis/supernova/__tests__/unit/qae.spec.js
+++ b/apis/supernova/__tests__/unit/qae.spec.js
@@ -85,7 +85,7 @@ describe('qae', () => {
data: {
targets: [
{
- path: 'qhc',
+ path: '/qHyperCubeDefFoo',
dimensions: {
min: () => 3,
max: () => 7,
@@ -105,7 +105,35 @@ describe('qae', () => {
],
},
})
- ).to.throw('Incorrect definition for qHyperCubeDef at qhc');
+ ).to.throw('Incorrect definition for qHyperCubeDef at /qHyperCubeDefFoo');
+ });
+ it('should throw with incorrect listobject def', () => {
+ expect(() =>
+ qae({
+ data: {
+ targets: [
+ {
+ path: '/qListObjectDefFoo',
+ dimensions: {
+ min: () => 3,
+ max: () => 7,
+ added: () => 'a',
+ description: () => 'Slice',
+ moved: () => 'c',
+ replaced: () => 'd',
+ },
+ measures: {
+ min: 2,
+ max: 4,
+ added: () => 'b',
+ description: () => 'Angle',
+ removed: () => 'e',
+ },
+ },
+ ],
+ },
+ })
+ ).to.throw('Incorrect definition for qListObjectDef at /qListObjectDefFoo');
});
it('should resolve layout', () => {
const t = qae({
diff --git a/apis/supernova/src/qae.js b/apis/supernova/src/qae.js
index ac81b4e96..8216492cf 100644
--- a/apis/supernova/src/qae.js
+++ b/apis/supernova/src/qae.js
@@ -66,8 +66,11 @@ const resolveValue = (data, reference, defaultValue) => {
function target(def) {
const propertyPath = def.path || '/qHyperCubeDef';
const layoutPath = propertyPath.slice(0, -3);
- if (/\/qHyperCube$/.test(layoutPath) === false) {
- throw new Error(`Incorrect definition for qHyperCubeDef at ${propertyPath}`);
+ if (/\/(qHyperCube|qListObject)$/.test(layoutPath) === false) {
+ const d = layoutPath.includes('/qHyperCube') ? 'qHyperCubeDef' : 'qListObjectDef';
+ throw new Error(
+ `Incorrect definition for ${d} at ${propertyPath}. Valid paths include /qHyperCubeDef or /qListObjectDef, e.g. data/qHyperCubeDef`
+ );
}
return {
propertyPath,
diff --git a/commands/serve/web/components/property-panel/Data.jsx b/commands/serve/web/components/property-panel/Data.jsx
index 90704e28c..7a02fc971 100644
--- a/commands/serve/web/components/property-panel/Data.jsx
+++ b/commands/serve/web/components/property-panel/Data.jsx
@@ -2,7 +2,7 @@ import React from 'react';
import { List, ListItem, Typography } from '@material-ui/core';
-import HyperCube from './HyperCube';
+import DataCube from './DataCube';
export default function Data({ setProperties, sn, properties }) {
if (!sn) {
@@ -19,7 +19,7 @@ export default function Data({ setProperties, sn, properties }) {
{targets.map((t) => (
-
+
))}
diff --git a/commands/serve/web/components/property-panel/HyperCube.jsx b/commands/serve/web/components/property-panel/DataCube.jsx
similarity index 86%
rename from commands/serve/web/components/property-panel/HyperCube.jsx
rename to commands/serve/web/components/property-panel/DataCube.jsx
index 5692af6ba..b98faa04a 100644
--- a/commands/serve/web/components/property-panel/HyperCube.jsx
+++ b/commands/serve/web/components/property-panel/DataCube.jsx
@@ -1,6 +1,7 @@
import React, { useMemo } from 'react';
import hcHandler from '@nebula.js/nucleus/src/object/hc-handler';
+import loHandler from '@nebula.js/nucleus/src/object/lo-handler';
import { Typography } from '@material-ui/core';
@@ -25,12 +26,13 @@ const getValue = (data, reference, defaultValue) => {
return dataContainer;
};
-export default function HyperCube({ setProperties, target, properties }) {
+export default function DataCube({ setProperties, target, properties }) {
+ const createHandler = target.propertyPath.match('/qHyperCube') ? hcHandler : loHandler;
const handler = useMemo(
() =>
- hcHandler({
+ createHandler({
def: target,
- hc: getValue(properties, target.propertyPath),
+ dc: getValue(properties, target.propertyPath),
properties,
}),
[properties]
diff --git a/commands/serve/web/components/property-panel/Fields.jsx b/commands/serve/web/components/property-panel/Fields.jsx
index edacdff68..7a42a3727 100644
--- a/commands/serve/web/components/property-panel/Fields.jsx
+++ b/commands/serve/web/components/property-panel/Fields.jsx
@@ -71,7 +71,7 @@ export default function Fields({
{label}
{items.map((d, i) => (
-
+
diff --git a/test/fixtures/viz/listbox/package.json b/test/fixtures/viz/listbox/package.json
new file mode 100644
index 000000000..07ae14238
--- /dev/null
+++ b/test/fixtures/viz/listbox/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "listbox",
+ "main": "dist/listbox.js",
+ "peerDependencies": {
+ "@nebula.js/stardust": "*"
+ }
+}
diff --git a/test/fixtures/viz/listbox/src/index.js b/test/fixtures/viz/listbox/src/index.js
new file mode 100644
index 000000000..69eb2ee75
--- /dev/null
+++ b/test/fixtures/viz/listbox/src/index.js
@@ -0,0 +1,139 @@
+import { useElement, useLayout, useEffect, useState, useSelections } from '@nebula.js/stardust';
+
+export default function v() {
+ return {
+ qae: {
+ properties: {
+ dimensionName: 'The first one',
+ foo: {
+ qListObjectDef: {
+ qShowAlternatives: true,
+ qInitialDataFetch: [
+ {
+ qLeft: 0,
+ qWidth: 1,
+ qTop: 0,
+ qHeight: 100,
+ },
+ ],
+ },
+ qDef: {
+ qSortCriterias: [
+ {
+ qSortByState: 1,
+ qSortByAscii: 1,
+ qSortByNumeric: 1,
+ qSortByLoadOrder: 1,
+ },
+ ],
+ },
+ },
+ },
+ data: {
+ targets: [
+ {
+ path: '/foo/qListObjectDef',
+ dimensions: {
+ min: 1,
+ max: 1,
+ description() {
+ return 'Your field';
+ },
+ },
+ },
+ ],
+ },
+ },
+ component() {
+ const element = useElement();
+ const layout = useLayout();
+ const selections = useSelections();
+ const [selectedRows, setSelectedRows] = useState([]);
+
+ useEffect(() => {
+ const listener = (e) => {
+ if (e.target.tagName === 'TD') {
+ if (!selections.isActive()) {
+ selections.begin('/foo/qListObjectDef');
+ }
+ const row = +e.target.parentElement.getAttribute('data-row');
+ const elemNumber = layout.foo.qListObject.qDataPages[0].qMatrix[row][0].qElemNumber;
+ setSelectedRows((prev) => {
+ if (prev.includes(elemNumber)) {
+ return prev.filter((pe) => pe !== elemNumber);
+ }
+ return [...prev, elemNumber];
+ });
+ }
+ };
+
+ element.addEventListener('click', listener);
+
+ return () => {
+ element.removeEventListener('click', listener);
+ };
+ }, [element]);
+
+ useEffect(() => {
+ const lo = layout.foo.qListObject;
+
+ // headers
+ const columns = [lo.qDimensionInfo.qFallbackTitle];
+ const header = `${columns.map((c) => `| ${c} | `).join('')}
`;
+
+ const STATES = {
+ S: {
+ background: 'rgba(0, 255, 0, 0.85)',
+ },
+ A: {
+ background: 'rgba(0, 0, 0, 0.3)',
+ },
+ };
+
+ // rows
+ const rows = lo.qDataPages[0].qMatrix
+ .map(
+ (row, ix) =>
+ `${row
+ .map(
+ (cell) =>
+ `| ${cell.qText} | `
+ )
+ .join('')}
`
+ )
+ .join('');
+
+ // table
+ const table = ``;
+
+ // output
+ element.innerHTML = table;
+ }, [element, layout]);
+
+ useEffect(() => {
+ if (selections.isActive()) {
+ if (selectedRows.length) {
+ selections.select({
+ method: 'selectListObjectValues',
+ params: ['/foo/qListObjectDef', selectedRows, false],
+ });
+ } else {
+ selections.select({
+ method: 'resetMadeSelections',
+ params: [],
+ });
+ }
+ } else if (selectedRows.length) {
+ setSelectedRows([]);
+ }
+ }, [selections.isActive(), selectedRows]);
+ },
+ };
+}