mirror of
https://github.com/qlik-oss/nebula.js.git
synced 2026-05-31 19:00:09 -04:00
feat: create api (#1079)
* feat: initial create obj api * chore: split create and generate * chore: remove duplicate selector * chore: set props to null at failed creation
This commit is contained in:
@@ -11,7 +11,8 @@ import ListBoxPopoverWrapper, {
|
||||
getOptions as getListboxPopoverOptions,
|
||||
} from './components/listbox/ListBoxPopoverWrapper';
|
||||
|
||||
import create from './object/create-session-object';
|
||||
import createSessionObject from './object/create-session-object';
|
||||
import createObject from './object/create-object';
|
||||
import get from './object/get-generic-object';
|
||||
import flagsFn from './flags/flags';
|
||||
import { create as typesFn } from './sn/types';
|
||||
@@ -251,8 +252,9 @@ function nuked(configuration = {}) {
|
||||
const api = /** @lends Embed# */ {
|
||||
/**
|
||||
* Renders a visualization or sheet into an HTMLElement.
|
||||
* Visualizations can either be existing objects or created on the fly.
|
||||
* Support for sense sheets is experimental.
|
||||
* @param {CreateConfig | GetConfig} cfg - The render configuration.
|
||||
* @param {RenderConfig} cfg The render configuration.
|
||||
* @returns {Promise<Viz|Sheet>} A controller to the rendered visualization or sheet.
|
||||
* @example
|
||||
* // render from existing object
|
||||
@@ -273,8 +275,39 @@ function nuked(configuration = {}) {
|
||||
if (cfg.id) {
|
||||
return get(cfg, halo);
|
||||
}
|
||||
return create(cfg, halo);
|
||||
return createSessionObject(cfg, halo);
|
||||
},
|
||||
/**
|
||||
* Creates a visualization model
|
||||
* @param {CreateConfig} cfg The create configuration.
|
||||
* @experimental
|
||||
* @returns {Promise<EngineAPI.IGenericObject>} An engima model
|
||||
* @example
|
||||
* // create a barchart in the app and return the model
|
||||
* const model = await n.create({
|
||||
* type: 'barchart',
|
||||
* fields: ['Product', { qLibraryId: 'u378hn', type: 'measure' }],
|
||||
* properties: { showTitle: true }
|
||||
* }
|
||||
* );
|
||||
*/
|
||||
create: async (cfg) => createObject(cfg, halo, false),
|
||||
/**
|
||||
* Generates properties for a visualization object
|
||||
* @param {CreateConfig} cfg The create configuration.
|
||||
* @experimental
|
||||
* @returns {Promise<object>} The objects properties
|
||||
* @example
|
||||
* // generate properties for a barchart
|
||||
* const properties = await n.create({
|
||||
* type: 'barchart',
|
||||
* fields: ['Product', { qLibraryId: 'u378hn', type: 'measure' }],
|
||||
* properties: { showTitle: true }
|
||||
* },
|
||||
* true
|
||||
* );
|
||||
*/
|
||||
generateProperties: async (cfg) => createObject(cfg, halo, true),
|
||||
/**
|
||||
* Updates the current context of this embed instance.
|
||||
* Use this when you want to change some part of the current context, like theme.
|
||||
|
||||
94
apis/nucleus/src/object/__tests__/create-object.test.js
Normal file
94
apis/nucleus/src/object/__tests__/create-object.test.js
Normal file
@@ -0,0 +1,94 @@
|
||||
import * as populatorModule from '../populator';
|
||||
import create from '../create-object';
|
||||
|
||||
describe('create-object', () => {
|
||||
let halo = {};
|
||||
let types;
|
||||
let sn;
|
||||
let merged;
|
||||
let populator;
|
||||
let init;
|
||||
let objectModel;
|
||||
|
||||
beforeEach(() => {
|
||||
populator = jest.fn();
|
||||
init = jest.fn();
|
||||
|
||||
jest.spyOn(populatorModule, 'default').mockImplementation(populator);
|
||||
objectModel = { id: 'id', on: () => {}, once: () => {} };
|
||||
types = {
|
||||
get: jest.fn(),
|
||||
};
|
||||
halo = {
|
||||
app: {
|
||||
createObject: jest.fn().mockResolvedValue(objectModel),
|
||||
},
|
||||
types,
|
||||
};
|
||||
|
||||
init.mockReturnValue('api');
|
||||
|
||||
sn = { qae: { properties: { onChange: jest.fn() } } };
|
||||
merged = { m: 'true' };
|
||||
const t = {
|
||||
initialProperties: jest.fn().mockResolvedValue(merged),
|
||||
supernova: jest.fn().mockResolvedValue(sn),
|
||||
};
|
||||
types.get.mockReturnValue(t);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('should call types.get with name and version', () => {
|
||||
create({ type: 't', version: 'v', fields: 'f' }, halo);
|
||||
expect(types.get).toHaveBeenCalledWith({ name: 't', version: 'v' });
|
||||
});
|
||||
|
||||
test('should call initialProperties on returned type', () => {
|
||||
const t = { initialProperties: jest.fn() };
|
||||
t.initialProperties.mockReturnValue({ then: () => {} });
|
||||
types.get.mockReturnValue(t);
|
||||
create({ type: 't', version: 'v', fields: 'f', properties: 'props', extendProperties: false }, halo);
|
||||
expect(t.initialProperties).toHaveBeenCalledWith('props', false);
|
||||
});
|
||||
|
||||
test('should populate fields', async () => {
|
||||
await create({ type: 't', version: 'v', fields: 'f', properties: 'props' }, halo);
|
||||
expect(populator).toHaveBeenCalledWith({ sn, properties: merged, fields: 'f' }, halo);
|
||||
});
|
||||
|
||||
test('should call properties onChange handler when optional props are provided', async () => {
|
||||
await create({ type: 't', version: 'v', fields: 'f', properties: 'props' }, halo);
|
||||
expect(sn.qae.properties.onChange).toHaveBeenCalledWith(merged);
|
||||
});
|
||||
|
||||
test('should not call onChange handler when optional props are not provided', async () => {
|
||||
await create({ type: 't', version: 'v', fields: 'f' }, halo);
|
||||
expect(sn.qae.properties.onChange).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('should create a object with merged props', async () => {
|
||||
await create({ type: 't', version: 'v', fields: 'f', properties: 'props' }, halo);
|
||||
expect(halo.app.createObject).toHaveBeenCalledWith(merged);
|
||||
});
|
||||
|
||||
test('should create a dummy object when error is thrown', async () => {
|
||||
types.get.mockImplementation(() => {
|
||||
throw new Error('oops');
|
||||
});
|
||||
await create({ type: 't', version: 'v', fields: 'f', properties: 'props' }, halo);
|
||||
expect(halo.app.createObject).toHaveBeenCalledWith({
|
||||
qInfo: { qType: 't' },
|
||||
visualization: 't',
|
||||
});
|
||||
});
|
||||
|
||||
test('should return props only', async () => {
|
||||
const props = await create({ type: 't', version: 'v', fields: 'f' }, halo, true);
|
||||
expect(halo.app.createObject).not.toHaveBeenCalledWith();
|
||||
expect(props).toEqual({ m: 'true' });
|
||||
});
|
||||
});
|
||||
63
apis/nucleus/src/object/create-object.js
Normal file
63
apis/nucleus/src/object/create-object.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import populateData from './populator';
|
||||
import { modelStore } from '../stores/model-store';
|
||||
|
||||
/**
|
||||
* @typedef {string | EngineAPI.INxDimension | EngineAPI.INxMeasure | LibraryField} Field
|
||||
*/
|
||||
|
||||
/**
|
||||
* @interface CreateConfig
|
||||
* @description Rendering configuration for creating and rendering a new object
|
||||
* @property {string} type
|
||||
* @property {string=} version
|
||||
* @property {(Field[])=} fields
|
||||
* @property {EngineAPI.IGenericObjectProperties=} properties
|
||||
*/
|
||||
|
||||
export default async function createObject(
|
||||
{ type, version, fields, properties, extendProperties /* , options, plugins, element */ },
|
||||
halo,
|
||||
generateOnly
|
||||
) {
|
||||
let mergedProps = {};
|
||||
// let error;
|
||||
try {
|
||||
const t = halo.types.get({ name: type, version });
|
||||
mergedProps = await t.initialProperties(properties, extendProperties);
|
||||
const sn = await t.supernova();
|
||||
if (fields) {
|
||||
populateData(
|
||||
{
|
||||
sn,
|
||||
properties: mergedProps,
|
||||
fields,
|
||||
},
|
||||
halo
|
||||
);
|
||||
}
|
||||
if (properties && sn && sn.qae.properties.onChange) {
|
||||
sn.qae.properties.onChange.call({}, mergedProps);
|
||||
}
|
||||
} catch (e) {
|
||||
// error = e;
|
||||
// minimal dummy object properties to allow it to be created
|
||||
// and rendered with the error
|
||||
if (!generateOnly) {
|
||||
mergedProps = {
|
||||
qInfo: {
|
||||
qType: type,
|
||||
},
|
||||
visualization: type,
|
||||
};
|
||||
} else {
|
||||
mergedProps = null;
|
||||
}
|
||||
// console.error(e); // eslint-disable-line
|
||||
}
|
||||
if (!generateOnly) {
|
||||
const model = await halo.app.createObject(mergedProps);
|
||||
modelStore.set(model.id, model);
|
||||
return model;
|
||||
}
|
||||
return mergedProps;
|
||||
}
|
||||
@@ -7,14 +7,17 @@ import { subscribe, modelStore } from '../stores/model-store';
|
||||
*/
|
||||
|
||||
/**
|
||||
* @interface CreateConfig
|
||||
* @description Rendering configuration for creating and rendering a new object
|
||||
* @extends BaseConfig
|
||||
* @property {string} type
|
||||
* @property {string=} version
|
||||
* @property {(Field[])=} fields
|
||||
* @property {boolean} [extendProperties=false] Whether to deeply extend properties or not. If false then subtrees will be overwritten.
|
||||
* @property {EngineAPI.IGenericObjectProperties=} properties
|
||||
* @interface RenderConfig
|
||||
* @description Configuration for rendering a visualisation, either creating or fetching an existing object.
|
||||
* @property {HTMLElement} element Target html element to render in to
|
||||
* @property {object=} options Options passed into the visualisation
|
||||
* @property {Plugin[]} [plugins] plugins passed into the visualisation
|
||||
* @property {string=} id For existing objects: Engine identifier of object to render
|
||||
* @property {string=} type For creating objects: Type of visualisation to render
|
||||
* @property {string=} version For creating objects: Version of visualization to render
|
||||
* @property {(Field[])=} fields For creating objects: Data fields to use
|
||||
* @property {boolean=} [extendProperties=false] For creating objects: Whether to deeply extend properties or not. If false then subtrees will be overwritten.
|
||||
* @property {EngineAPI.IGenericObjectProperties=} properties For creating objects: Explicit properties to set
|
||||
* @example
|
||||
* // A config for Creating objects:
|
||||
* const createConfig = {
|
||||
@@ -29,6 +32,12 @@ import { subscribe, modelStore } from '../stores/model-store';
|
||||
* }
|
||||
* };
|
||||
* nebbie.render(createConfig);
|
||||
* // A config for rendering an existing object:
|
||||
* const createConfig = {
|
||||
* id: 'jG5LP',
|
||||
* element: document.querySelector('.line'),
|
||||
* };
|
||||
* nebbie.render(createConfig);
|
||||
*/
|
||||
export default async function createSessionObject(
|
||||
{ type, version, fields, properties, options, plugins, element, extendProperties },
|
||||
|
||||
@@ -2,21 +2,6 @@ import init from './initiate';
|
||||
import initSheet from './initiate-sheet';
|
||||
import { modelStore, rpcRequestModelStore } from '../stores/model-store';
|
||||
|
||||
/**
|
||||
* @interface BaseConfig
|
||||
* @description Basic rendering configuration for rendering an object
|
||||
* @property {HTMLElement} element
|
||||
* @property {object=} options
|
||||
* @property {Plugin[]} [plugins]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @interface GetConfig
|
||||
* @description Rendering configuration for rendering an existing object
|
||||
* @extends BaseConfig
|
||||
* @property {string} id
|
||||
*/
|
||||
|
||||
export default async function getObject({ id, options, plugins, element }, halo) {
|
||||
const key = `${id}`;
|
||||
let rpc = rpcRequestModelStore.get(key);
|
||||
|
||||
@@ -71,7 +71,7 @@ export default function viz({ model, halo, initialError, onDestroy = async () =>
|
||||
id: model.id,
|
||||
/**
|
||||
* This visualizations Enigma model, a representation of the generic object.
|
||||
* @type {string}
|
||||
* @type {EngineAPI.IGenericObject}
|
||||
*/
|
||||
model,
|
||||
/**
|
||||
|
||||
@@ -692,21 +692,13 @@
|
||||
},
|
||||
"entries": {
|
||||
"render": {
|
||||
"description": "Renders a visualization or sheet into an HTMLElement.\nSupport for sense sheets is experimental.",
|
||||
"description": "Renders a visualization or sheet into an HTMLElement.\nVisualizations can either be existing objects or created on the fly.\nSupport for sense sheets is experimental.",
|
||||
"kind": "function",
|
||||
"params": [
|
||||
{
|
||||
"name": "cfg",
|
||||
"description": "The render configuration.",
|
||||
"kind": "union",
|
||||
"items": [
|
||||
{
|
||||
"type": "#/definitions/CreateConfig"
|
||||
},
|
||||
{
|
||||
"type": "#/definitions/GetConfig"
|
||||
}
|
||||
]
|
||||
"type": "#/definitions/RenderConfig"
|
||||
}
|
||||
],
|
||||
"returns": {
|
||||
@@ -731,6 +723,54 @@
|
||||
"// render on the fly\nn.render({\n element: el,\n type: 'barchart',\n fields: ['Product', { qLibraryId: 'u378hn', type: 'measure' }]\n});"
|
||||
]
|
||||
},
|
||||
"create": {
|
||||
"description": "Creates a visualization model",
|
||||
"stability": "experimental",
|
||||
"kind": "function",
|
||||
"params": [
|
||||
{
|
||||
"name": "cfg",
|
||||
"description": "The create configuration.",
|
||||
"type": "#/definitions/CreateConfig"
|
||||
}
|
||||
],
|
||||
"returns": {
|
||||
"description": "An engima model",
|
||||
"type": "Promise",
|
||||
"generics": [
|
||||
{
|
||||
"type": "EngineAPI.IGenericObject"
|
||||
}
|
||||
]
|
||||
},
|
||||
"examples": [
|
||||
"// create a barchart in the app and return the model\nconst model = await n.create({\n type: 'barchart',\n fields: ['Product', { qLibraryId: 'u378hn', type: 'measure' }],\n properties: { showTitle: true }\n }\n);"
|
||||
]
|
||||
},
|
||||
"generateProperties": {
|
||||
"description": "Generates properties for a visualization object",
|
||||
"stability": "experimental",
|
||||
"kind": "function",
|
||||
"params": [
|
||||
{
|
||||
"name": "cfg",
|
||||
"description": "The create configuration.",
|
||||
"type": "#/definitions/CreateConfig"
|
||||
}
|
||||
],
|
||||
"returns": {
|
||||
"description": "The objects properties",
|
||||
"type": "Promise",
|
||||
"generics": [
|
||||
{
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"examples": [
|
||||
"// generate properties for a barchart\nconst properties = await n.create({\n type: 'barchart',\n fields: ['Product', { qLibraryId: 'u378hn', type: 'measure' }],\n properties: { showTitle: true }\n },\n true\n);"
|
||||
]
|
||||
},
|
||||
"context": {
|
||||
"description": "Updates the current context of this embed instance.\nUse this when you want to change some part of the current context, like theme.",
|
||||
"kind": "function",
|
||||
@@ -1152,7 +1192,7 @@
|
||||
},
|
||||
"model": {
|
||||
"description": "This visualizations Enigma model, a representation of the generic object.",
|
||||
"type": "string"
|
||||
"type": "EngineAPI.IGenericObject"
|
||||
},
|
||||
"destroy": {
|
||||
"description": "Destroys the visualization and removes it from the the DOM.",
|
||||
@@ -1474,11 +1514,6 @@
|
||||
},
|
||||
"CreateConfig": {
|
||||
"description": "Rendering configuration for creating and rendering a new object",
|
||||
"extends": [
|
||||
{
|
||||
"type": "#/definitions/BaseConfig"
|
||||
}
|
||||
],
|
||||
"kind": "interface",
|
||||
"entries": {
|
||||
"type": {
|
||||
@@ -1500,54 +1535,76 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"extendProperties": {
|
||||
"description": "Whether to deeply extend properties or not. If false then subtrees will be overwritten.",
|
||||
"optional": true,
|
||||
"defaultValue": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"properties": {
|
||||
"optional": true,
|
||||
"type": "EngineAPI.IGenericObjectProperties"
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
"// A config for Creating objects:\nconst createConfig = {\n type: 'bar',\n element: document.querySelector('.bar'),\n extendProperties: true,\n fields: ['[Country names]', '=Sum(Sales)'],\n properties: {\n legend: {\n show: false,\n },\n }\n};\nnebbie.render(createConfig);"
|
||||
]
|
||||
}
|
||||
},
|
||||
"BaseConfig": {
|
||||
"description": "Basic rendering configuration for rendering an object",
|
||||
"RenderConfig": {
|
||||
"description": "Configuration for rendering a visualisation, either creating or fetching an existing object.",
|
||||
"kind": "interface",
|
||||
"entries": {
|
||||
"element": {
|
||||
"description": "Target html element to render in to",
|
||||
"type": "HTMLElement"
|
||||
},
|
||||
"options": {
|
||||
"description": "Options passed into the visualisation",
|
||||
"optional": true,
|
||||
"type": "object"
|
||||
},
|
||||
"plugins": {
|
||||
"description": "plugins passed into the visualisation",
|
||||
"optional": true,
|
||||
"kind": "array",
|
||||
"items": {
|
||||
"type": "#/definitions/Plugin"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"GetConfig": {
|
||||
"description": "Rendering configuration for rendering an existing object",
|
||||
"extends": [
|
||||
{
|
||||
"type": "#/definitions/BaseConfig"
|
||||
}
|
||||
],
|
||||
"kind": "interface",
|
||||
"entries": {
|
||||
},
|
||||
"id": {
|
||||
"description": "For existing objects: Engine identifier of object to render",
|
||||
"optional": true,
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"description": "For creating objects: Type of visualisation to render",
|
||||
"optional": true,
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"description": "For creating objects: Version of visualization to render",
|
||||
"optional": true,
|
||||
"type": "string"
|
||||
},
|
||||
"fields": {
|
||||
"description": "For creating objects: Data fields to use",
|
||||
"optional": true,
|
||||
"kind": "union",
|
||||
"items": [
|
||||
{
|
||||
"kind": "array",
|
||||
"items": {
|
||||
"type": "#/definitions/Field"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"extendProperties": {
|
||||
"description": "For creating objects: Whether to deeply extend properties or not. If false then subtrees will be overwritten.",
|
||||
"optional": true,
|
||||
"defaultValue": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"properties": {
|
||||
"description": "For creating objects: Explicit properties to set",
|
||||
"optional": true,
|
||||
"type": "EngineAPI.IGenericObjectProperties"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
"// A config for Creating objects:\nconst createConfig = {\n type: 'bar',\n element: document.querySelector('.bar'),\n extendProperties: true,\n fields: ['[Country names]', '=Sum(Sales)'],\n properties: {\n legend: {\n show: false,\n },\n }\n};\nnebbie.render(createConfig);\n// A config for rendering an existing object:\nconst createConfig = {\n id: 'jG5LP',\n element: document.querySelector('.line'),\n};\nnebbie.render(createConfig);"
|
||||
]
|
||||
},
|
||||
"LibraryField": {
|
||||
"kind": "interface",
|
||||
|
||||
37
apis/stardust/types/index.d.ts
vendored
37
apis/stardust/types/index.d.ts
vendored
@@ -235,10 +235,23 @@ declare namespace stardust {
|
||||
|
||||
/**
|
||||
* Renders a visualization or sheet into an HTMLElement.
|
||||
* Visualizations can either be existing objects or created on the fly.
|
||||
* Support for sense sheets is experimental.
|
||||
* @param cfg The render configuration.
|
||||
*/
|
||||
render(cfg: stardust.CreateConfig | stardust.GetConfig): Promise<stardust.Viz | stardust.Sheet>;
|
||||
render(cfg: stardust.RenderConfig): Promise<stardust.Viz | stardust.Sheet>;
|
||||
|
||||
/**
|
||||
* Creates a visualization model
|
||||
* @param cfg The create configuration.
|
||||
*/
|
||||
create(cfg: stardust.CreateConfig): Promise<EngineAPI.IGenericObject>;
|
||||
|
||||
/**
|
||||
* Generates properties for a visualization object
|
||||
* @param cfg The create configuration.
|
||||
*/
|
||||
generateProperties(cfg: stardust.CreateConfig): Promise<object>;
|
||||
|
||||
/**
|
||||
* Updates the current context of this embed instance.
|
||||
@@ -357,7 +370,7 @@ declare namespace stardust {
|
||||
|
||||
id: string;
|
||||
|
||||
model: string;
|
||||
model: EngineAPI.IGenericObject;
|
||||
|
||||
/**
|
||||
* Destroys the visualization and removes it from the the DOM.
|
||||
@@ -460,28 +473,26 @@ declare namespace stardust {
|
||||
/**
|
||||
* Rendering configuration for creating and rendering a new object
|
||||
*/
|
||||
interface CreateConfig extends stardust.BaseConfig{
|
||||
interface CreateConfig {
|
||||
type: string;
|
||||
version?: string;
|
||||
fields?: stardust.Field[];
|
||||
extendProperties?: boolean;
|
||||
properties?: EngineAPI.IGenericObjectProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic rendering configuration for rendering an object
|
||||
* Configuration for rendering a visualisation, either creating or fetching an existing object.
|
||||
*/
|
||||
interface BaseConfig {
|
||||
interface RenderConfig {
|
||||
element: HTMLElement;
|
||||
options?: object;
|
||||
plugins?: stardust.Plugin[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendering configuration for rendering an existing object
|
||||
*/
|
||||
interface GetConfig extends stardust.BaseConfig{
|
||||
id: string;
|
||||
id?: string;
|
||||
type?: string;
|
||||
version?: string;
|
||||
fields?: stardust.Field[];
|
||||
extendProperties?: boolean;
|
||||
properties?: EngineAPI.IGenericObjectProperties;
|
||||
}
|
||||
|
||||
interface LibraryField {
|
||||
|
||||
@@ -1010,7 +1010,6 @@ export function onTakeSnapshot(cb) {
|
||||
* @ignore
|
||||
* @example
|
||||
* import { onContextMenu } from '@nebula.js/stardust';
|
||||
|
||||
* onContextMenu((menu, event) => {
|
||||
* menu.addItem(item, index);
|
||||
* });
|
||||
|
||||
Reference in New Issue
Block a user