From c0daa041cfc5abe4ccad7df7f75c331b910ecce5 Mon Sep 17 00:00:00 2001 From: Quan Ho Date: Mon, 2 Nov 2020 09:47:11 +0100 Subject: [PATCH] feat: support ListObject (#526) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: support `ListObject` * chore: add listbox fixture * refactor: refactor * refactor: refactor * refactor: update error message Co-authored-by: Christoffer Åström Co-authored-by: Tobias Åström --- apis/nucleus/src/components/Cell.jsx | 24 +-- .../src/object/__tests__/hc-handler.spec.js | 2 +- .../src/object/__tests__/populator.spec.js | 2 +- apis/nucleus/src/object/hc-handler.js | 14 +- apis/nucleus/src/object/lo-handler.js | 72 +++++++++ apis/nucleus/src/object/populator.js | 2 +- apis/supernova/__tests__/unit/qae.spec.js | 32 +++- apis/supernova/src/qae.js | 7 +- .../web/components/property-panel/Data.jsx | 4 +- .../{HyperCube.jsx => DataCube.jsx} | 8 +- .../web/components/property-panel/Fields.jsx | 2 +- test/fixtures/viz/listbox/package.json | 7 + test/fixtures/viz/listbox/src/index.js | 139 ++++++++++++++++++ 13 files changed, 283 insertions(+), 32 deletions(-) create mode 100644 apis/nucleus/src/object/lo-handler.js rename commands/serve/web/components/property-panel/{HyperCube.jsx => DataCube.jsx} (86%) create mode 100644 test/fixtures/viz/listbox/package.json create mode 100644 test/fixtures/viz/listbox/src/index.js 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 = `${header}${rows}
`; + + // 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]); + }, + }; +}