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:
Quan Ho
2020-11-02 09:47:11 +01:00
committed by GitHub
parent c7529f5b11
commit c0daa041cf
13 changed files with 283 additions and 32 deletions

View File

@@ -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');

View File

@@ -25,7 +25,7 @@ describe('hc-handler', () => {
};
h = handler({
hc,
dc: hc,
def,
properties: 'props',
});

View File

@@ -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 } } },
});

View File

@@ -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;
}

View 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;
}

View File

@@ -45,7 +45,7 @@ export default function populateData({ sn, properties, fields }) {
}
const hc = hcHandler({
hc: p,
dc: p,
def: target,
properties,
});

View File

@@ -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({

View File

@@ -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,

View File

@@ -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>

View File

@@ -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]

View File

@@ -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>

View 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
View 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]);
},
};
}