diff --git a/apis/nucleus/src/components/listbox/ListBoxInline.jsx b/apis/nucleus/src/components/listbox/ListBoxInline.jsx index fd7593d96..6341bf699 100644 --- a/apis/nucleus/src/components/listbox/ListBoxInline.jsx +++ b/apis/nucleus/src/components/listbox/ListBoxInline.jsx @@ -21,50 +21,59 @@ import InstanceContext from '../../contexts/InstanceContext'; import ListBoxSearch from './ListBoxSearch'; import useObjectSelections from '../../hooks/useObjectSelections'; -export default function ListBoxPortal({ app, fieldName, stateName, element, options }) { +export default function ListBoxPortal({ app, fieldIdentifier, stateName, element, options }) { return ReactDOM.createPortal( - , + , element ); } -export function ListBoxInline({ app, fieldName, stateName = '$', options = {} }) { - const theme = useTheme(); +export function ListBoxInline({ app, fieldIdentifier, stateName = '$', options = {} }) { const { title, direction, listLayout, search = true } = options; - const [model] = useSessionModel( - { - qInfo: { - qType: 'njsListbox', - }, - qListObjectDef: { - qStateName: stateName, - qShowAlternatives: true, - qInitialDataFetch: [ + const listdef = { + qInfo: { + qType: 'njsListbox', + }, + qListObjectDef: { + qStateName: stateName, + qShowAlternatives: true, + qInitialDataFetch: [ + { + qTop: 0, + qLeft: 0, + qWidth: 0, + qHeight: 0, + }, + ], + qDef: { + qSortCriterias: [ { - qTop: 0, - qLeft: 0, - qWidth: 0, - qHeight: 0, + qSortByState: 1, + qSortByAscii: 1, + qSortByNumeric: 1, + qSortByLoadOrder: 1, }, ], - qDef: { - qSortCriterias: [ - { - qSortByState: 1, - qSortByAscii: 1, - qSortByNumeric: 1, - qSortByLoadOrder: 1, - }, - ], - qFieldDefs: [fieldName], - }, }, - title, }, - app, - fieldName, - stateName - ); + title, + }; + + let fieldName; + let isMaster = false; + + // Something something lib dimension + if (fieldIdentifier.qLibraryId) { + listdef.qListObjectDef.qLibraryId = fieldIdentifier.qLibraryId; + fieldName = fieldIdentifier.qLibraryId; + isMaster = true; + } else { + listdef.qListObjectDef.qDef.qFieldDefs = [fieldIdentifier]; + fieldName = fieldIdentifier; + } + + const theme = useTheme(); + const [model] = useSessionModel(listdef, app, fieldName, stateName); const lock = useCallback(() => { model.lock('/qListObjectDef'); @@ -137,7 +146,7 @@ export function ListBoxInline({ app, fieldName, stateName = '$', options = {} }) {showTitle && ( - {layout.title || fieldName} + {!isMaster ? layout.title || fieldName : layout.qListObject.qDimensionInfo.qFallbackTitle} )} diff --git a/apis/nucleus/src/components/listbox/__tests__/list-box-column.spec.jsx b/apis/nucleus/src/components/listbox/__tests__/list-box-column.spec.jsx new file mode 100644 index 000000000..553f1694f --- /dev/null +++ b/apis/nucleus/src/components/listbox/__tests__/list-box-column.spec.jsx @@ -0,0 +1,320 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import { Grid, Typography } from '@material-ui/core'; +import Lock from '@nebula.js/ui/icons/lock'; + +const [{ default: ListBoxColumn }] = aw.mock( + [ + [ + require.resolve('@nebula.js/ui/theme'), + () => ({ + makeStyles: () => () => ({ + S: 'selected', + A: 'alternative', + X: 'excluded', + highlighted: 'highlighted', + }), + }), + ], + ], + ['../ListBoxRow'] +); + +describe('', () => { + it('should have default props', () => { + const index = 0; + const style = {}; + const data = { + onClick: sinon.spy(), + pages: [], + }; + const testRenderer = renderer.create(); + const testInstance = testRenderer.root; + + const type = testInstance.findByType(Grid); + expect(type.props.container).to.equal(true); + expect(type.props.spacing).to.equal(0); + expect(type.props.style).to.deep.equal({}); + expect(type.props.role).to.equal('row'); + expect(type.props.className).to.equal(''); + expect(type.props.onClick.callCount).to.equal(0); + + const types = testInstance.findAllByType(Typography); + expect(types).to.have.length(1); + expect(types[0].props.component).to.equal('span'); + expect(types[0].props.noWrap).to.equal(true); + expect(types[0].props.children).to.equal(''); + }); + it('should set locked state', () => { + const index = 0; + const style = {}; + const data = { + onClick: sinon.spy(), + pages: [ + { + qArea: { + qLeft: 0, + qTop: 0, + qWidth: 0, + qHeight: 100, + }, + qMatrix: [ + [ + { + qState: 'L', + }, + ], + ], + }, + ], + }; + const testRenderer = renderer.create(); + const testInstance = testRenderer.root; + + const type = testInstance.findByType(Lock); + expect(type.props.size).to.equal('small'); + }); + it('should set selected', () => { + const index = 0; + const style = {}; + const data = { + onClick: sinon.spy(), + pages: [ + { + qArea: { + qLeft: 0, + qTop: 0, + qWidth: 0, + qHeight: 100, + }, + qMatrix: [ + [ + { + qState: 'L', + }, + ], + ], + }, + ], + }; + const testRenderer = renderer.create(); + const testInstance = testRenderer.root; + const type = testInstance.findByType(Grid); + expect(type.props.className).to.equal('selected'); + }); + it('should set alternative', () => { + const index = 0; + const style = {}; + const data = { + onClick: sinon.spy(), + pages: [ + { + qArea: { + qLeft: 0, + qTop: 0, + qWidth: 0, + qHeight: 100, + }, + qMatrix: [ + [ + { + qState: 'A', + }, + ], + ], + }, + ], + }; + const testRenderer = renderer.create(); + const testInstance = testRenderer.root; + const type = testInstance.findByType(Grid); + expect(type.props.className).to.equal('alternative'); + }); + it('should set excluded - qState X', () => { + const index = 0; + const style = {}; + const data = { + onClick: sinon.spy(), + pages: [ + { + qArea: { + qLeft: 0, + qTop: 0, + qWidth: 0, + qHeight: 100, + }, + qMatrix: [ + [ + { + qState: 'X', + }, + ], + ], + }, + ], + }; + const testRenderer = renderer.create(); + const testInstance = testRenderer.root; + const type = testInstance.findByType(Grid); + expect(type.props.className).to.equal('excluded'); + }); + it('should set excluded - qState XS', () => { + const index = 0; + const style = {}; + const data = { + onClick: sinon.spy(), + pages: [ + { + qArea: { + qLeft: 0, + qTop: 0, + qWidth: 0, + qHeight: 100, + }, + qMatrix: [ + [ + { + qState: 'XS', + }, + ], + ], + }, + ], + }; + const testRenderer = renderer.create(); + const testInstance = testRenderer.root; + const type = testInstance.findByType(Grid); + expect(type.props.className).to.equal('excluded'); + }); + it('should set excluded - qState XL', () => { + const index = 0; + const style = {}; + const data = { + onClick: sinon.spy(), + pages: [ + { + qArea: { + qLeft: 0, + qTop: 0, + qWidth: 0, + qHeight: 100, + }, + qMatrix: [ + [ + { + qState: 'XL', + }, + ], + ], + }, + ], + }; + const testRenderer = renderer.create(); + const testInstance = testRenderer.root; + const type = testInstance.findByType(Grid); + expect(type.props.className).to.equal('excluded'); + }); + it('should highlight ranges', () => { + const index = 0; + const style = {}; + const data = { + onClick: sinon.spy(), + pages: [ + { + qArea: { + qLeft: 0, + qTop: 0, + qWidth: 0, + qHeight: 100, + }, + qMatrix: [ + [ + { + qState: '', + qText: 'nebula.js ftw', + qHighlightRanges: { + qRanges: [{ qCharPos: 0, qCharCount: 9 }], + }, + }, + ], + ], + }, + ], + }; + const testRenderer = renderer.create(); + const testInstance = testRenderer.root; + const types = testInstance.findAllByType(Typography); + expect(types[0].props.children).to.equal('nebula.js'); + expect(types[0].props.className).to.equal('highlighted'); + expect(types[1].props.children).to.equal(' ftw'); + }); + it('should highlight ranges', () => { + const index = 0; + const style = {}; + const data = { + onClick: sinon.spy(), + pages: [ + { + qArea: { + qLeft: 0, + qTop: 0, + qWidth: 0, + qHeight: 100, + }, + qMatrix: [ + [ + { + qState: '', + qText: 'nebula.js ftw', + qHighlightRanges: { + qRanges: [{ qCharPos: 10, qCharCount: 3 }], + }, + }, + ], + ], + }, + ], + }; + const testRenderer = renderer.create(); + const testInstance = testRenderer.root; + const types = testInstance.findAllByType(Typography); + expect(types[0].props.children).to.equal('nebula.js '); + expect(types[1].props.children).to.equal('ftw'); + expect(types[1].props.className).to.equal('highlighted'); + }); + it('should highlight ranges', () => { + const index = 0; + const style = {}; + const data = { + onClick: sinon.spy(), + pages: [ + { + qArea: { + qLeft: 0, + qTop: 0, + qWidth: 0, + qHeight: 100, + }, + qMatrix: [ + [ + { + qState: '', + qText: 'nebula.js ftw yeah buddy', + qHighlightRanges: { + qRanges: [{ qCharPos: 14, qCharCount: 4 }], + }, + }, + ], + ], + }, + ], + }; + const testRenderer = renderer.create(); + const testInstance = testRenderer.root; + const types = testInstance.findAllByType(Typography); + expect(types[0].props.children).to.equal('nebula.js ftw '); + expect(types[1].props.children).to.equal('yeah'); + expect(types[1].props.className).to.equal('highlighted'); + expect(types[2].props.children).to.equal(' buddy'); + }); +}); diff --git a/apis/nucleus/src/index.js b/apis/nucleus/src/index.js index 7d8db8a0b..d11ba6cc4 100644 --- a/apis/nucleus/src/index.js +++ b/apis/nucleus/src/index.js @@ -311,19 +311,24 @@ function nuked(configuration = {}) { return selectionsApi; }, /** - * Gets the instance of the specified field - * @param {string} fieldName - * @returns {Promise} + * Gets the listbox instance of the specified field + * @param {string|LibraryField} fieldIdentifier Fieldname as a string or a Library dimension + * @returns {Promise} * @experimental * @example - * const field = await n.field("MyField"); - * field.mount(element, { title: "Hello Field"}); + * const listbox = await n.listbox("MyField"); + * listbox.mount(element, { title: "Hello Field"}); */ - field: async (fieldName) => { + listbox: async (fieldIdentifier) => { + const fieldName = typeof fieldIdentifier === 'string' ? fieldIdentifier : fieldIdentifier.qLibraryId; + if (!fieldName) { + throw new Error(`Field identifier must be provided`); + } + /** * @class * @hideconstructor - * @alias FieldSelections + * @alias ListboxInstance * @experimental */ const fieldSels = { @@ -338,7 +343,7 @@ function nuked(configuration = {}) { * @param {boolean=} [options.search=true] To show the search bar * @experimental * @example - * field.mount(element); + * listbox.mount(element); */ mount(element, options = {}) { if (!element) { @@ -350,7 +355,7 @@ function nuked(configuration = {}) { this._instance = ListBoxPortal({ element, app, - fieldName, + fieldIdentifier, options, }); root.add(this._instance); @@ -359,7 +364,7 @@ function nuked(configuration = {}) { * Unmounts the field listbox from the DOM. * @experimental * @example - * field.unmount(); + * listbox.unmount(); */ unmount() { if (this._instance) { diff --git a/apis/nucleus/src/object/create-session-object.js b/apis/nucleus/src/object/create-session-object.js index 7ad8d0895..ba552b8be 100644 --- a/apis/nucleus/src/object/create-session-object.js +++ b/apis/nucleus/src/object/create-session-object.js @@ -1,6 +1,11 @@ import populateData from './populator'; import init from './initiate'; import { subscribe, modelStore } from '../stores/model-store'; + +/** + * @typedef {string | qae.NxDimension | qae.NxMeasure | LibraryField} Field + */ + /** * @interface CreateConfig * @description Rendering configuration for creating and rendering a new object @@ -10,7 +15,6 @@ import { subscribe, modelStore } from '../stores/model-store'; * @property {(Field[])=} fields * @property {qae.GenericObjectProperties=} properties */ - export default async function createSessionObject({ type, version, fields, properties, options, element }, halo) { let mergedProps = {}; let error; diff --git a/apis/nucleus/src/object/populator.js b/apis/nucleus/src/object/populator.js index 64e1a2111..d0b96a8d1 100644 --- a/apis/nucleus/src/object/populator.js +++ b/apis/nucleus/src/object/populator.js @@ -1,9 +1,5 @@ import hcHandler from './hc-handler'; -/** - * @typedef {(string | qae.NxDimension | qae.NxMeasure | LibraryField)} Field - */ - /** * @interface LibraryField * @property {string} qLibraryId diff --git a/apis/stardust/api-spec/spec.json b/apis/stardust/api-spec/spec.json index 9103e268a..1e4eba1de 100644 --- a/apis/stardust/api-spec/spec.json +++ b/apis/stardust/api-spec/spec.json @@ -605,7 +605,7 @@ "optional": true, "kind": "array", "items": { - "type": "Field" + "type": "#/definitions/Field" } }, "properties": { @@ -688,25 +688,37 @@ "// limit constraints\nn.context({ constraints: { active: true } });" ] }, - "field": { - "description": "Gets the instance of the specified field", + "listbox": { + "description": "Gets the listbox instance of the specified field", "stability": "experimental", "kind": "function", "params": [ { - "name": "fieldName", - "type": "string" + "name": "fieldIdentifier", + "description": "Fieldname as a string or a Library dimension", + "kind": "union", + "items": [ + { + "type": "string" + }, + { + "type": "#/definitions/LibraryField" + } + ], + "type": "any" } ], "returns": { "type": "Promise", "generics": [ { - "type": "#/definitions/FieldSelections" + "type": "#/definitions/ListboxInstance" } ] }, - "examples": ["const field = await n.field(\"MyField\");\nfield.mount(element, { title: \"Hello Field\"});"] + "examples": [ + "const listbox = await n.listbox(\"MyField\");\nlistbox.mount(element, { title: \"Hello Field\"});" + ] }, "render": { "description": "Renders a visualization into an HTMLElement.", @@ -834,66 +846,23 @@ "type": "#/definitions/ExportFormat" } }, - "FieldSelections": { - "stability": "experimental", - "kind": "class", - "constructor": { - "kind": "function", - "params": [] - }, - "entries": {}, - "staticEntries": { - "mount": { - "description": "Mounts the field as a listbox into the provided HTMLElement.", - "stability": "experimental", - "kind": "function", - "params": [ - { - "name": "element", - "type": "HTMLElement" - }, - { - "name": "options", - "description": "Settings for the embedded listbox", - "optional": true, - "kind": "object", - "entries": { - "title": { - "description": "Custom title, defaults to fieldname", - "optional": true, - "type": "string" - }, - "direction": { - "description": "Direction setting ltr|rtl.", - "optional": true, - "defaultValue": "ltr", - "type": "string" - }, - "listLayout": { - "description": "Layout direction vertical|horizontal", - "optional": true, - "defaultValue": "vertical", - "type": "string" - }, - "search": { - "description": "To show the search bar", - "optional": true, - "defaultValue": true, - "type": "boolean" - } - } - } - ], - "examples": ["field.mount(element);"] + "Field": { + "kind": "union", + "items": [ + { + "type": "string" }, - "unmount": { - "description": "Unmounts the field listbox from the DOM.", - "stability": "experimental", - "kind": "function", - "params": [], - "examples": ["field.unmount();"] + { + "type": "qae.NxDimension" + }, + { + "type": "qae.NxMeasure" + }, + { + "type": "#/definitions/LibraryField" } - } + ], + "type": "any" }, "FieldTarget": { "kind": "interface", @@ -1036,6 +1005,67 @@ } } }, + "ListboxInstance": { + "stability": "experimental", + "kind": "class", + "constructor": { + "kind": "function", + "params": [] + }, + "entries": {}, + "staticEntries": { + "mount": { + "description": "Mounts the field as a listbox into the provided HTMLElement.", + "stability": "experimental", + "kind": "function", + "params": [ + { + "name": "element", + "type": "HTMLElement" + }, + { + "name": "options", + "description": "Settings for the embedded listbox", + "optional": true, + "kind": "object", + "entries": { + "title": { + "description": "Custom title, defaults to fieldname", + "optional": true, + "type": "string" + }, + "direction": { + "description": "Direction setting ltr|rtl.", + "optional": true, + "defaultValue": "ltr", + "type": "string" + }, + "listLayout": { + "description": "Layout direction vertical|horizontal", + "optional": true, + "defaultValue": "vertical", + "type": "string" + }, + "search": { + "description": "To show the search bar", + "optional": true, + "defaultValue": true, + "type": "boolean" + } + } + } + ], + "examples": ["listbox.mount(element);"] + }, + "unmount": { + "description": "Unmounts the field listbox from the DOM.", + "stability": "experimental", + "kind": "function", + "params": [], + "examples": ["listbox.unmount();"] + } + } + }, "LoadType": { "kind": "interface", "params": [