diff --git a/commands/sense/README.md b/commands/sense/README.md index c45d24251..3a3ac5dd7 100644 --- a/commands/sense/README.md +++ b/commands/sense/README.md @@ -22,7 +22,6 @@ Options: --output Destination directory [string] [default: "-ext"] --minify Minify and uglify code [boolean] [default: true] --sourcemap Generate sourcemaps [boolean] [default: false] - --legacy Generate legacy extension [boolean] [default: false] -h, --help Show help [boolean] ``` @@ -123,21 +122,3 @@ example: "supernova": true } ``` - -The `"supernova": true` attribute should not be added when building with the ---legacy option below. - -### Legacy - -Qlik Sense before 2020 does not support nebula supernova natively, so a special -build path is needed for visualizations to load properly in the client. -For this purpose, use the --legacy option. - -```bash -nebula sense --legacy -``` - -You can find that the generated `QEXT` file does not include `supernova: true`. - -Note: -For old Qlik Sense, not all features of extension are presented. diff --git a/commands/sense/lib/build-legacy.js b/commands/sense/lib/build-legacy.js deleted file mode 100644 index 4dc6e7e4e..000000000 --- a/commands/sense/lib/build-legacy.js +++ /dev/null @@ -1,116 +0,0 @@ -const path = require('path'); -const fs = require('fs-extra'); - -const rollup = require('rollup'); -const { nodeResolve } = require('@rollup/plugin-node-resolve'); -const common = require('@rollup/plugin-commonjs'); -const replace = require('@rollup/plugin-replace'); -const babel = require('@rollup/plugin-babel'); -const terser = require('@rollup/plugin-terser'); - -async function build(argv) { - const cwd = process.cwd(); - - const supernovaPkg = require(path.resolve(cwd, 'package.json')); // eslint-disable-line - - let extName = supernovaPkg.name.split('/').reverse()[0]; - - const { main } = supernovaPkg; - - if (extName === main) { - extName = extName.replace(/\.js$/, '-ext.js'); - } - - const targetDir = argv.output !== '-ext' ? argv.output : `${extName}-ext`; - const qextLegacyTargetDir = path.resolve(cwd, targetDir); - const qextFileName = path.resolve(qextLegacyTargetDir, `${extName}.qext`); - const qextFileNameJs = qextFileName.replace(/\.qext$/, '.js'); - - const extDefinition = argv.ext ? path.resolve(argv.ext) : ''; - - const relativeMainFile = `./${main.replace(/\.js$/, '')}`; - - async function moveSnBundle() { - const code = await fs.readFile(path.resolve(cwd, main), { encoding: 'utf8' }); - const replacedCode = code.replace(/@nebula.js\/stardust/g, '../nlib/@nebula.js/stardust/dist/stardust'); - - await fs.outputFile(path.resolve(qextLegacyTargetDir, main), replacedCode); - } - - async function moveResources() { - await fs.copy( - path.resolve(path.dirname(require.resolve('@nebula.js/stardust')), 'dist'), - path.resolve(qextLegacyTargetDir, 'nlib/@nebula.js/stardust/dist') - ); - } - - async function createQextFiles() { - const qext = supernovaPkg.qext || {}; - if (argv.meta) { - const meta = require(path.resolve(cwd, argv.meta)); // eslint-disable-line - Object.assign(qext, meta); - } - const contents = { - name: qext.name || extName, - version: supernovaPkg.version, - description: supernovaPkg.description, - author: supernovaPkg.author, - icon: qext.icon || 'extension', - preview: qext.preview, - type: 'visualization', - }; - - await fs.writeFile(qextFileName, JSON.stringify(contents, null, 2)); - } - - async function wrapIt() { - const bundle = await rollup.rollup({ - input: path.resolve(__dirname, '../src/legacy/sn-ext.js'), - external: ['translator', 'qlik', './nlib/@nebula.js/stardust/dist/stardust', relativeMainFile], - plugins: [ - replace({ - __SN_DEF__: `${relativeMainFile}`, - __EXT_DEF__: `${extDefinition.replace(/\\/g, '/')}`, - preventAssignment: true, - }), - nodeResolve(), - common(), - babel({ - babelHelpers: 'bundled', - babelrc: false, - exclude: [/node_modules/], - presets: [ - [ - '@babel/preset-env', - { - modules: false, - targets: { - browsers: ['chrome 62'], - }, - }, - ], - ], - }), - argv.minify && - terser({ - output: { - comments: /@license|@preserve|Copyright|license/, - }, - }), - ], - }); - - await bundle.write({ - file: qextFileNameJs, - format: 'amd', - sourcemap: argv.sourcemap, - }); - } - - await moveSnBundle(); - await moveResources(); - await wrapIt(); - await createQextFiles(); -} - -module.exports = build; diff --git a/commands/sense/lib/init-config.js b/commands/sense/lib/init-config.js index fb0f7007d..8198f9084 100644 --- a/commands/sense/lib/init-config.js +++ b/commands/sense/lib/init-config.js @@ -27,12 +27,6 @@ const options = { default: false, desc: 'Generate sourcemaps', }, - legacy: { - type: 'boolean', - required: false, - default: false, - desc: 'Generate legacy extension', - }, }; module.exports = (yargs) => yargs.options(options); diff --git a/commands/sense/lib/sense.js b/commands/sense/lib/sense.js index ac8a6bb67..1a939654f 100644 --- a/commands/sense/lib/sense.js +++ b/commands/sense/lib/sense.js @@ -1,9 +1,9 @@ -const buildLegacy = require('./build-legacy'); const build = require('./build'); function sense(argv) { if (argv.legacy) { - return buildLegacy(argv); + console.error('Legacy sense-build support removed in 4.0, it is not required for sense compatability.'); + process.exit(1); } return build(argv); } diff --git a/commands/sense/src/legacy/__tests__/selections.test.js b/commands/sense/src/legacy/__tests__/selections.test.js deleted file mode 100644 index a11f7ccd6..000000000 --- a/commands/sense/src/legacy/__tests__/selections.test.js +++ /dev/null @@ -1,63 +0,0 @@ -import createSelections from '../selections'; - -describe('supernova-wrapper', () => { - describe('selections', () => { - let selectionsApi; - let selectionsApiActivateMock; - let selectionsApiDeactivateMock; - - let backendApi; - let backendApiBeganSelectionMock; - let backendApiEndSelectionMock; - let backendApiResetSelectionMock; - let backendApiSwitchModalSelectionMock; - - let selections; - let selectionsEmitMock; - - beforeEach(() => { - selectionsApiActivateMock = jest.fn(); - selectionsApiDeactivateMock = jest.fn(); - - backendApiBeganSelectionMock = jest.fn(); - backendApiEndSelectionMock = jest.fn(); - backendApiResetSelectionMock = jest.fn(); - backendApiSwitchModalSelectionMock = jest.fn(); - - selectionsEmitMock = jest.fn(); - - selectionsApi = { - activated: selectionsApiActivateMock, - deactivated: selectionsApiDeactivateMock, - }; - backendApi = { - beginSelections: backendApiBeganSelectionMock, - endSelections: backendApiEndSelectionMock, - model: { - resetMadeSelections: backendApiResetSelectionMock, - app: { - switchModalSelection: backendApiSwitchModalSelectionMock, - }, - }, - }; - selections = createSelections({ selectionsApi, backendApi }); - selections.emit = selectionsEmitMock; - }); - - it('should `begin`', () => { - selections.begin(); - expect(selectionsApi.activated).toHaveBeenCalledWith(true); - expect(backendApi.beginSelections).toHaveBeenCalledWith(); - expect(selections.emit).toHaveBeenCalledWith('activated'); - }); - - it('should `begin` with paths', () => { - const paths = ['hypercubePath', 'otherHypercubePath']; - selections.begin(paths); - expect(selectionsApi.activated).toHaveBeenCalledWith(true); - expect(backendApi.beginSelections).toHaveBeenCalledTimes(0); - expect(backendApi.model.app.switchModalSelection).toHaveBeenCalledWith(backendApi.model, paths); - expect(selections.emit).toHaveBeenCalledWith('activated'); - }); - }); -}); diff --git a/commands/sense/src/legacy/empty-ext.js b/commands/sense/src/legacy/empty-ext.js deleted file mode 100644 index f4052b9a9..000000000 --- a/commands/sense/src/legacy/empty-ext.js +++ /dev/null @@ -1,3 +0,0 @@ -const x = {}; - -export default x; diff --git a/commands/sense/src/legacy/permissions.js b/commands/sense/src/legacy/permissions.js deleted file mode 100644 index ccbbf5c55..000000000 --- a/commands/sense/src/legacy/permissions.js +++ /dev/null @@ -1,36 +0,0 @@ -const INTERACTION_STATES = { - STATIC: 0, - ANALYSIS: 1, - EDIT: 2, -}; - -function showTooltip(options) { - if (options.tooltips) { - return true; - } - return options.interactionState === INTERACTION_STATES.ANALYSIS && options.tooltips !== false; -} - -function permissions(options, backendApi) { - const p = []; - if (showTooltip(options)) { - p.push('passive'); - } - if ( - options.interactionState === INTERACTION_STATES.ANALYSIS && - options.tooltips !== false && - options.limitedInteraction !== true - ) { - p.push('interact'); - } - if (!backendApi.isSnapshot) { - if (options.selections !== false) { - p.push('select'); - } - p.push('fetch'); - } - - return p; -} - -export default permissions; diff --git a/commands/sense/src/legacy/selections.js b/commands/sense/src/legacy/selections.js deleted file mode 100644 index be839107d..000000000 --- a/commands/sense/src/legacy/selections.js +++ /dev/null @@ -1,82 +0,0 @@ -/* eslint no-param-reassign: 0 */ -import EventEmitter from 'node-event-emitter'; - -export default (scope) => { - const mixin = (obj) => { - Object.keys(EventEmitter.prototype).forEach((key) => { - obj[key] = EventEmitter.prototype[key]; - }); - EventEmitter.init(obj); - return obj; - }; - - // TODO one and only one - - const selectionAPI = { - begin(paths) { - const suppressBeginSelections = true; - scope.selectionsApi.activated(suppressBeginSelections); - if (paths) { - scope.backendApi.model.app.switchModalSelection(scope.backendApi.model, paths); - } else { - scope.backendApi.beginSelections(); - } - selectionAPI.emit('activated'); - }, - clear() { - scope.backendApi.model.resetMadeSelections(); - }, - confirm() { - scope.selectionsApi.confirm(); - }, - cancel() { - scope.selectionsApi.cancel(); - }, - select(s) { - if (!scope.selectionsApi.active) { - scope.backendApi.beginSelections(); - } - scope.backendApi.model[s.method](...s.params).then((qSuccess) => { - if (!qSuccess) { - scope.selectionsApi.selectionsMade = false; - this.clear(); - } - }); - scope.selectionsApi.selectionsMade = s.method !== 'resetMadeSelections'; - }, - isActive() { - return scope.selectionsApi.active; - }, - isModal() { - return scope.backendApi.model === (scope.backendApi.model.app && scope.backendApi.model.app.modalSelectionObject); - }, - refreshToolbar() { - scope.selectionsApi.refreshToolbar(); - }, - }; - - mixin(selectionAPI); - - scope.selectionsApi.confirm = () => { - scope.backendApi.endSelections(true).then(() => { - scope.selectionsApi.deactivated(); - selectionAPI.emit('confirmed'); - selectionAPI.emit('deactivated'); - }); - }; - - scope.selectionsApi.clear = () => { - scope.backendApi.clearSelections(); - scope.selectionsApi.selectionsMade = false; - selectionAPI.emit('cleared'); - }; - - scope.selectionsApi.cancel = () => { - scope.backendApi.endSelections(false); - scope.selectionsApi.deactivated(); - selectionAPI.emit('canceled'); - selectionAPI.emit('deactivated'); - }; - - return selectionAPI; -}; diff --git a/commands/sense/src/legacy/sn-ext.js b/commands/sense/src/legacy/sn-ext.js deleted file mode 100644 index f23af6b71..000000000 --- a/commands/sense/src/legacy/sn-ext.js +++ /dev/null @@ -1,291 +0,0 @@ -/* eslint no-underscore-dangle: 0 */ -/* eslint import/no-unresolved: 0 */ -/* eslint import/extensions: 0 */ - -// Sense dependencies -import qlik from 'qlik'; -import senseTranslator from 'translator'; - -// injected -import snDefinition from '__SN_DEF__'; -import extDefinition from '__EXT_DEF__'; -import emptyExtDefinition from './empty-ext'; - -// lib dependencies -import { __DO_NOT_USE__ } from './nlib/@nebula.js/stardust/dist/stardust'; - -// wrapper code -import permissions from './permissions'; -import selectionsApi from './selections'; - -const { generator: supernova, theme: themeFn, locale: localeFn } = __DO_NOT_USE__; - -// ------- locale --------- -// use nebula locale API to ensure all viz get the same API regardless -// if consumed in Sense or outside -const loc = localeFn({ - initial: senseTranslator.language, -}); -const translator = { - ...loc.translator, - // wrap get() for this API in order to prefer strings provided by Sense - get(str, args) { - const s = senseTranslator.get(str, args); - if (s === str) { - return loc.translator.get(str, args); - } - return s; - }, -}; -// ------------------------ - -// Galaxy is usually provided by @nebula.js/nucleus when a viz is instantiated. -// Since nucleus is not being used in Sense yet, this interface MUST be provided explicitly here -// and it MUST have the same interface as the one provided by nucleus. -const galaxy = { - deviceType: 'auto', - translator, - flags: { - isEnabled: () => false, - }, - anything: {}, -}; - -const ALLOWED_OPTIONS = ['viewState', 'direction', 'renderer']; -function limitOptions(newOptions, oldOptions) { - const op = {}; - let opChanged = false; - Object.keys(newOptions).forEach((key) => { - if (ALLOWED_OPTIONS.indexOf(key) === -1) { - return; - } - op[key] = newOptions[key]; - if (oldOptions[key] !== newOptions[key]) { - opChanged = true; - } - }); - if (opChanged) { - return op; - } - return oldOptions; -} - -function createActions(actions) { - return actions.map((a) => { - const senseItem = { - isIcon: true, - buttonClass: 'sel-toolbar-icon-toggle', - isActive() { - return a.active; - }, - isDisabled() { - return a.disabled; - }, - action() { - a.action(); - }, - getSvgIcon() { - const icon = a.getSvgIconShape(); - const { viewBox = '0 0 16 16' } = icon; - const shapes = icon.shapes - .map( - ({ type, attrs }) => - `<${type} ${Object.keys(attrs) - .map((k) => `${k}="${attrs[k]}"`) - .join(' ')}/>` - ) - .join(''); - return `${shapes}`; - }, - }; - Object.defineProperties(senseItem, { - name: { - get: () => a.label, - }, - hidden: { - get: () => a.hidden, - }, - }); - return senseItem; - }); -} - -function updateTheme(ref) { - return qlik - .currApp(ref) - .theme.getApplied() - .then((qTheme) => { - if (ref.nTheme.externalAPI.name() !== qTheme.id) { - ref.nTheme.internalAPI.setTheme(qTheme.properties, qTheme.id); - } - }); -} - -// ============= EXTENSON ===================================================== - -const snGenerator = supernova(snDefinition, galaxy); -const ext = - typeof extDefinition === 'function' - ? extDefinition({ translator }) - : extDefinition || snGenerator.definition.ext || emptyExtDefinition || {}; -let data; - -if (snGenerator.qae.data.targets[0]) { - const d = snGenerator.qae.data.targets[0]; - // supernova uses the convention of 'added' instead of 'add' since the - // method is called after the dimension/measures has been added to the hypercube. - // we therefore need to map supernova callbacks to the ones supported in Sense - const map = (v) => ({ - ...v, - add: v.added, - remove: v.removed, - replace: v.replaced, - move: v.moved, - }); - - data = {}; - if (d.dimensions) { - data.dimensions = map(d.dimensions); - } - if (d.measures) { - data.measures = map(d.measures); - } -} - -const X = { - // overridable properties - definition: { - type: 'items', - component: 'accordion', - items: { - data: data - ? { - uses: 'data', - } - : undefined, - sorting: data - ? { - uses: 'sorting', - } - : undefined, - settings: { - uses: 'settings', - }, - }, - }, - data, - // -- defaults for the following are injected in default-extension.js -- - // support - // importProperties, - // exportProperties, - // ---- - getSelectionToolbar() { - return [ - ...this.senseItems, - { - name: 'Tooltip.clearSelection', - tid: 'selection-toolbar.clear', - isIcon: true, - buttonClass: 'clear', - iconClass: 'lui-icon lui-icon--clear-selections', - action: function action() { - this.selectionsApi.clear(); - }, - isDisabled: function isDisabled() { - return !this.selectionsApi.selectionsMade; - }, - }, - ]; - }, - // override with user config - ...ext, - - // ============================================= - // non-overridable properties - initialProperties: snGenerator.qae.properties.initial, - template: '
', - mounted($element) { - const element = $element[0].children[0]; - const selectionAPI = selectionsApi(this.$scope); - const sn = snGenerator.create({ - model: this.backendApi.model, - app: this.backendApi.model.session.app, - selections: selectionAPI, - explicitResize: true, - }); - this.sn = sn; - this.snComponent = sn.component; - this.snComponent.created(); - this.snComponent.mounted(element); - this.senseItems = []; - const { senseItems } = this; - this.snComponent.observeActions((actions) => { - senseItems.length = 0; - senseItems.push(...createActions(actions)); - this.$scope.selectionsApi.refreshToolbar(); - }); - - this.nTheme = themeFn(); - - this.initiatedTheme = updateTheme(this); - }, - suppressOnPaint: () => false, - paint() { - const perms = permissions(this.options, this.backendApi); - const constraints = { - passive: perms.indexOf('passive') === -1 || undefined, - active: perms.indexOf('interact') === -1 || undefined, - select: perms.indexOf('select') === -1 || undefined, - edit: perms.indexOf('edit') === -1 || undefined, - }; - const opts = limitOptions(this.options, this.snComponent.context.options); - this._pureLayout = this.backendApi.model.pureLayout || this.backendApi.model.layout; - return this.initiatedTheme.then(() => { - updateTheme(this); - return this.snComponent.render({ - layout: this.backendApi.model.pureLayout || this.backendApi.model.layout, - context: { - appLayout: { - qLocaleInfo: this.backendApi.localeInfo, - }, - constraints, - theme: this.nTheme.externalAPI, - }, - options: opts, - }); - }); - }, - resize() { - if (this._pureLayout !== this.backendApi.model.pureLayout) { - return this.paint(); - } - const opts = limitOptions(this.options, this.snComponent.context.options); - this.snComponent.context.options = opts; - return this.snComponent.resize(); - }, - setInteractionState() { - this.paint(); - }, - setSnapshotData(layout) { - return this.snComponent.setSnapshotData(layout); - }, - onContextMenu(menu, event) { - return this.snComponent.onContextMenu(menu, event); - }, - getViewState() { - const ref = this.snComponent.getImperativeHandle(); - if (ref && typeof ref.getViewState === 'function') { - return ref.getViewState(); - } - return undefined; - }, - destroy() { - this.sn.destroy(); - if (this.snComponent) { - this.snComponent.willUnmount(); - this.snComponent.destroy(); - } - }, -}; - -export default X;