mirror of
https://github.com/qlik-oss/nebula.js.git
synced 2025-12-19 09:48:18 -05:00
feat: remove sense legacy build path (#1244)
This commit is contained in:
@@ -22,7 +22,6 @@ Options:
|
||||
--output Destination directory [string] [default: "<name>-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.
|
||||
|
||||
@@ -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 !== '<name>-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;
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
const x = {};
|
||||
|
||||
export default x;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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 `<i style="display:inline-block;font-style:normal;line-height:0;text-align:center;text-transform:none;vertical-align:-.2em;text-rendering:optimizeLegibility;web-kit-font-smoothing:antialiased;moz-osx-font-smoothing:grayscale"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="${viewBox}" fill="currentColor">${shapes}</svg></i>`;
|
||||
},
|
||||
};
|
||||
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: '<div style="height: 100%;position: relative"></div>',
|
||||
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;
|
||||
Reference in New Issue
Block a user