mirror of
https://github.com/qlik-oss/nebula.js.git
synced 2026-05-24 16:00:16 -04:00
feat: support ListObject (#526)
* feat: support `ListObject` * chore: add listbox fixture * refactor: refactor * refactor: refactor * refactor: update error message Co-authored-by: Christoffer Åström <stoffeastrom@gmail.com> Co-authored-by: Tobias Åström <tsm@qlik.com>
This commit is contained in:
@@ -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');
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('hc-handler', () => {
|
||||
};
|
||||
|
||||
h = handler({
|
||||
hc,
|
||||
dc: hc,
|
||||
def,
|
||||
properties: 'props',
|
||||
});
|
||||
|
||||
@@ -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 } } },
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
72
apis/nucleus/src/object/lo-handler.js
Normal file
72
apis/nucleus/src/object/lo-handler.js
Normal file
@@ -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;
|
||||
}
|
||||
@@ -45,7 +45,7 @@ export default function populateData({ sn, properties, fields }) {
|
||||
}
|
||||
|
||||
const hc = hcHandler({
|
||||
hc: p,
|
||||
dc: p,
|
||||
def: target,
|
||||
properties,
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }) {
|
||||
<List>
|
||||
{targets.map((t) => (
|
||||
<ListItem key={t.propertyPath} divider disableGutters>
|
||||
<HyperCube target={t} properties={properties} setProperties={setProperties} />
|
||||
<DataCube target={t} properties={properties} setProperties={setProperties} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
|
||||
@@ -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]
|
||||
@@ -71,7 +71,7 @@ export default function Fields({
|
||||
<Typography variant="overline">{label}</Typography>
|
||||
<List dense>
|
||||
{items.map((d, i) => (
|
||||
<ListItem disableGutters key={d.qDef.cId || i}>
|
||||
<ListItem disableGutters key={(d.qDef && d.qDef.cId) || i}>
|
||||
<ListItemText>
|
||||
<FieldTitle field={d} libraryItems={libraryItems} type={type} />
|
||||
</ListItemText>
|
||||
|
||||
7
test/fixtures/viz/listbox/package.json
vendored
Normal file
7
test/fixtures/viz/listbox/package.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "listbox",
|
||||
"main": "dist/listbox.js",
|
||||
"peerDependencies": {
|
||||
"@nebula.js/stardust": "*"
|
||||
}
|
||||
}
|
||||
139
test/fixtures/viz/listbox/src/index.js
vendored
Normal file
139
test/fixtures/viz/listbox/src/index.js
vendored
Normal file
@@ -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 = `<thead><tr>${columns.map((c) => `<th>${c}</th>`).join('')}</tr></thead>`;
|
||||
|
||||
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) =>
|
||||
`<tr data-row=${ix}>${row
|
||||
.map(
|
||||
(cell) =>
|
||||
`<td style="background:${
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
cell.qState === 'S' || cell.qState === 'L'
|
||||
? STATES.S.background
|
||||
: cell.qState === 'A'
|
||||
? STATES.A.background
|
||||
: 'none'
|
||||
}">${cell.qText}</td>`
|
||||
)
|
||||
.join('')}</tr>`
|
||||
)
|
||||
.join('');
|
||||
|
||||
// table
|
||||
const table = `<table>${header}<tbody>${rows}</tbody></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]);
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user