Compare commits
89 Commits
snapshotFi
...
QB-377/err
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ab340f3f1 | ||
|
|
8ba826a0ea | ||
|
|
d2446395e2 | ||
|
|
f17a7b7714 | ||
|
|
8aa86275f0 | ||
|
|
3ebc2b9e29 | ||
|
|
ce81549011 | ||
|
|
e64af66dab | ||
|
|
fd9e645fc1 | ||
|
|
ca1b1a9b53 | ||
|
|
a65f843008 | ||
|
|
be9570e0aa | ||
|
|
41d3a7c9af | ||
|
|
1e0a7c1204 | ||
|
|
17b5df296c | ||
|
|
64b778b1af | ||
|
|
be6718ccf4 | ||
|
|
28e6237a43 | ||
|
|
4fab3b5933 | ||
|
|
79339a8963 | ||
|
|
a6faeeb656 | ||
|
|
c22b7f5c6b | ||
|
|
80f97602e4 | ||
|
|
f745656b4c | ||
|
|
f7e780b92e | ||
|
|
d7a76c7db9 | ||
|
|
16c380e1c6 | ||
|
|
072a3b80c4 | ||
|
|
5572b7ce67 | ||
|
|
71cf92c217 | ||
|
|
e26d5fded8 | ||
|
|
20282b0b99 | ||
|
|
5f18321ccf | ||
|
|
d14f5951ac | ||
|
|
5872ee7b58 | ||
|
|
1436463f59 | ||
|
|
729a31920d | ||
|
|
6d305b21b2 | ||
|
|
0b210e0d35 | ||
|
|
c8afb83130 | ||
|
|
49981f6ae3 | ||
|
|
b3b17e8d0c | ||
|
|
5d45f57e00 | ||
|
|
2f59d97cf3 | ||
|
|
b8d6b0a53e | ||
|
|
6cc82e7b38 | ||
|
|
cc7e3e62ed | ||
|
|
26de3b63ed | ||
|
|
8eafeffcec | ||
|
|
ef7926dd13 | ||
|
|
af307fd24b | ||
|
|
98401db922 | ||
|
|
75771e4815 | ||
|
|
a12205c840 | ||
|
|
ebfee69c7b | ||
|
|
05c25c72cb | ||
|
|
d2a24dd256 | ||
|
|
a6a6ef24b5 | ||
|
|
8374ecce85 | ||
|
|
0135d4fc64 | ||
|
|
47d7f33cb9 | ||
|
|
4c2e483592 | ||
|
|
801c7c862e | ||
|
|
f3bda74202 | ||
|
|
3ec5d13ae6 | ||
|
|
2133bb44ef | ||
|
|
e447666982 | ||
|
|
7038140088 | ||
|
|
b5d9633496 | ||
|
|
a518432db4 | ||
|
|
f1875702be | ||
|
|
b1a9130663 | ||
|
|
dfb30285ab | ||
|
|
c8cd78ae5f | ||
|
|
a5bc3ecd1b | ||
|
|
bec68e7cd4 | ||
|
|
02bba4ad5a | ||
|
|
57c4b12b24 | ||
|
|
ca33540fd6 | ||
|
|
cac3fabb2f | ||
|
|
c57b0edea8 | ||
|
|
3862bd294c | ||
|
|
0c18523891 | ||
|
|
629821bd6b | ||
|
|
08c5cf8104 | ||
|
|
d3c39bea75 | ||
|
|
141be3f962 | ||
|
|
433a725f33 | ||
|
|
34527c3d6d |
@@ -20,15 +20,12 @@ jobs:
|
|||||||
command: npm install
|
command: npm install
|
||||||
- run:
|
- run:
|
||||||
name: BlackDuck scan
|
name: BlackDuck scan
|
||||||
command: curl -s https://blackducksoftware.github.io/hub-detect/hub-detect.sh | bash -s -- \
|
command: curl -s https://detect.synopsys.com/detect.sh | bash -s -- \
|
||||||
--blackduck.url="https://qliktech.blackducksoftware.com" \
|
--blackduck.url="https://qliktech.blackducksoftware.com" \
|
||||||
--blackduck.trust.cert=true \
|
--blackduck.trust.cert=true \
|
||||||
--blackduck.username="svc-blackduck" \
|
--blackduck.username="svc-blackduck" \
|
||||||
--blackduck.password=${svc_blackduck} \
|
--blackduck.password=${svc_blackduck} \
|
||||||
--detect.project.name="viz-bundle-qlik-smart-pivot"
|
--detect.project.name="viz-bundle-qlik-smart-pivot"
|
||||||
- run:
|
|
||||||
name: Run tests
|
|
||||||
command: npm run test-once
|
|
||||||
|
|
||||||
bump-version:
|
bump-version:
|
||||||
<<: *defaults
|
<<: *defaults
|
||||||
@@ -56,16 +53,18 @@ jobs:
|
|||||||
command: |
|
command: |
|
||||||
export VERSION=$(scripts/get-bumped-version.sh)
|
export VERSION=$(scripts/get-bumped-version.sh)
|
||||||
echo "Version: ${VERSION}"
|
echo "Version: ${VERSION}"
|
||||||
npm run build
|
npm run build:zip
|
||||||
|
sudo chmod +x scripts/verify-files.sh
|
||||||
|
scripts/verify-files.sh
|
||||||
environment:
|
environment:
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
- persist_to_workspace:
|
- persist_to_workspace:
|
||||||
root: ~/qlik-smart-pivot
|
root: ~/qlik-smart-pivot
|
||||||
paths:
|
paths:
|
||||||
- build
|
- dist
|
||||||
- store_artifacts:
|
- store_artifacts:
|
||||||
path: build
|
path: dist
|
||||||
destination: build
|
destination: dist
|
||||||
deploy:
|
deploy:
|
||||||
<<: *defaults
|
<<: *defaults
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -19,7 +19,7 @@ $RECYCLE.BIN/
|
|||||||
|
|
||||||
# Temporary build files
|
# Temporary build files
|
||||||
node_modules/
|
node_modules/
|
||||||
build/
|
dist/
|
||||||
BUMPED_VERSION
|
BUMPED_VERSION
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
|
|||||||
49
README.md
49
README.md
@@ -1,57 +1,22 @@
|
|||||||
# P&L Smart Pivot, a Qlik Sense Extension for Financial reporting
|
# P&L Smart Pivot, a Qlik Sense Extension for Financial reporting
|
||||||
[](https://circleci.com/gh/qlik-oss/PLSmartPivot)
|
This extension is part of the extension bundles for Qlik Sense. The repository is maintained and moderated by Qlik RD.
|
||||||
|
|
||||||
This extension is useful to create reports where the look&feel is rellevantand and pivot a second dimension is needed. Based on P&L Smart.
|
Feel free to fork and suggest pull requests for improvements and bug fixes. Changes will be moderated and reviewed before inclusion in future bundle versions. Please note that emphasis is on backward compatibility, i.e. breaking changes will most likely not be approved.
|
||||||
|
|
||||||
It's specifically focused on financial reports, trying to solve some common needs of this area:
|
|
||||||
- smart export to excel
|
|
||||||
- easy creation of reports
|
|
||||||
- custom corporate reporting (bold, italic, background color, letter size, headers,...)
|
|
||||||
- selections inside the reports
|
|
||||||
- custom external templates
|
|
||||||
- analytical reports
|
|
||||||
|
|
||||||
|
|
||||||
# Manual
|
|
||||||
You'll find a manual [Qlik Sense P&LSmart Pivot Extension Manual.pdf](resources/Qlik Sense P&LSmart Pivot Extension Manual.pdf) and one app example [P&LSmartPivot_demo.qvf](resources/P&LSmartPivot_demo.qvf).
|
|
||||||
|
|
||||||
|
|
||||||
# If the import does not work at first time
|
|
||||||
- remove [Accounts.csv](resources/Accounts.csv), [Accounts2.csv](resources/Accounts2.csv) and [Excel.png](resources/Excel.png), zip it again and import.
|
|
||||||
- Then reintroduce [Accounts.csv](resources/Accounts.csv), [Accounts2.csv](resources/Accounts2.csv) and [Excel.png](resources/Excel.png), zip it again and import.
|
|
||||||
|
|
||||||
|
|
||||||
# Installation
|
|
||||||
|
|
||||||
1. Download the extension zip, `qlik-smart-pivot_<version>.zip`, from the latest release(https://github.com/qlik-oss/PLSmartPivot/releases/latest)
|
|
||||||
2. Install the extension:
|
|
||||||
|
|
||||||
a. **Qlik Sense Desktop**: unzip to a directory under [My Documents]/Qlik/Sense/Extensions.
|
|
||||||
|
|
||||||
b. **Qlik Sense Server**: import the zip file in the QMC.
|
|
||||||
|
|
||||||
|
Usage documentation for the extension is available at https://help.qlik.com.
|
||||||
|
|
||||||
# Developing the extension
|
# Developing the extension
|
||||||
|
|
||||||
If you want to do code changes to the extension follow these simple steps to get going.
|
If you want to do code changes to the extension follow these simple steps to get going.
|
||||||
|
|
||||||
1. Get Qlik Sense Desktop
|
1. Get Qlik Sense Desktop
|
||||||
1. Create a new app and add the extension to a sheet.
|
1. Create a new app and add P&L pivot to a sheet.
|
||||||
2. Clone the repository
|
2. Clone the repository
|
||||||
3. Run `npm install`
|
3. Run `npm install`
|
||||||
4. Set the environment variable `BUILD_PATH` to your extensions directory. It will be something like `C:/Users/<user>/Documents/Qlik/Sense/Extensions/<extension_name>`.
|
4. Run `npm run build` - to build a dev-version to the /dist folder.
|
||||||
5. You now have two options. Either run the watch task or the build task. They are explained below. Both of them default to development mode but can be run in production by setting `NODE_ENV=production` before running the npm task.
|
5. Move the content of the /dist folder to the extension directory. Usually in `C:/Users/<user>/Documents/Qlik/Sense/Extensions/qlik-smart-pivot`.
|
||||||
|
|
||||||
a. **Watch**: `npm run watch`. This will start a watcher which will rebuild the extension and output all needed files to the `buildFolder` for each code change you make. See your changes directly in your Qlik Sense app.
|
|
||||||
|
|
||||||
b. **Build**: `npm run build`. If you want to build the extension package. The output zip-file can be found in the `buildFolder`.
|
|
||||||
|
|
||||||
|
|
||||||
# Original authors
|
# Original authors
|
||||||
|
|
||||||
[github.com/iviasensio](https://github.com/iviasensio)
|
[github.com/iviasensio](https://github.com/iviasensio)
|
||||||
|
|
||||||
|
|
||||||
# License
|
# License
|
||||||
|
Released under the [MIT License](LICENSE).
|
||||||
Released under the [MIT License](LICENSE).
|
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "P&L pivot",
|
|
||||||
"description": "Profit & Loss reporting with color and font customizations.",
|
|
||||||
"type": "visualization",
|
|
||||||
"version": "X.Y.Z",
|
|
||||||
"icon": "table",
|
|
||||||
"preview": "",
|
|
||||||
"author": "Ivan Felipe Asensio <ivan.felipe@qlik.com>",
|
|
||||||
"homepage": "",
|
|
||||||
"keywords": "qlik-sense, visualization",
|
|
||||||
"license": "MIT",
|
|
||||||
"repository": "https://github.com/qlik-oss/PLSmartPivot",
|
|
||||||
"dependencies": {
|
|
||||||
"qlik-sense": ">=5.5.x"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
qlik-smart-pivot.js;
|
|
||||||
qlik-smart-pivot.css;
|
|
||||||
qlik-smart-pivot.qext
|
|
||||||
94
gulpfile.js
94
gulpfile.js
@@ -1,23 +1,61 @@
|
|||||||
var gulp = require('gulp');
|
var gulp = require('gulp');
|
||||||
|
var gutil = require('gulp-util');
|
||||||
var zip = require('gulp-zip');
|
var zip = require('gulp-zip');
|
||||||
var del = require('del');
|
var del = require('del');
|
||||||
var path = require('path');
|
|
||||||
var settings = require('./settings');
|
|
||||||
var webpackConfig = require('./webpack.config');
|
var webpackConfig = require('./webpack.config');
|
||||||
var webpack = require('webpack');
|
var webpack = require('webpack');
|
||||||
var WebpackDevServer = require('webpack-dev-server');
|
var pkg = require('./package.json');
|
||||||
var jeditor = require("gulp-json-editor");
|
|
||||||
|
|
||||||
var srcFiles = path.resolve('./src/**/*.*');
|
var DIST = './dist';
|
||||||
|
var VERSION = process.env.VERSION || 'local-dev';
|
||||||
|
|
||||||
gulp.task('remove-build-folder', function(){
|
gulp.task('qext', function () {
|
||||||
return del([settings.buildDestination], { force: true });
|
var qext = {
|
||||||
|
name: 'P&L pivot',
|
||||||
|
type: 'visualization',
|
||||||
|
description: pkg.description + '\nVersion: ' + VERSION,
|
||||||
|
version: VERSION,
|
||||||
|
icon: 'pivot-table',
|
||||||
|
preview: 'qlik-smart-pivot.png',
|
||||||
|
keywords: 'qlik-sense, visualization',
|
||||||
|
author: pkg.author,
|
||||||
|
homepage: pkg.homepage,
|
||||||
|
license: pkg.license,
|
||||||
|
repository: pkg.repository,
|
||||||
|
dependencies: {
|
||||||
|
'qlik-sense': '>=5.5.x'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (pkg.contributors) {
|
||||||
|
qext.contributors = pkg.contributors;
|
||||||
|
}
|
||||||
|
var src = require('stream').Readable({
|
||||||
|
objectMode: true
|
||||||
|
});
|
||||||
|
src._read = function () {
|
||||||
|
this.push(new gutil.File({
|
||||||
|
cwd: '',
|
||||||
|
base: '',
|
||||||
|
path: pkg.name + '.qext',
|
||||||
|
contents: Buffer.from(JSON.stringify(qext, null, 4))
|
||||||
|
}));
|
||||||
|
this.push(null);
|
||||||
|
};
|
||||||
|
return src.pipe(gulp.dest(DIST));
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('clean', function(){
|
||||||
|
return del([DIST], { force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task('zip-build', function(){
|
gulp.task('zip-build', function(){
|
||||||
return gulp.src(settings.buildDestination + '/**/*')
|
return gulp.src(DIST + '/**/*')
|
||||||
.pipe(zip(`${settings.name}_${settings.version}.zip`))
|
.pipe(zip(`${pkg.name}_${VERSION}.zip`))
|
||||||
.pipe(gulp.dest(settings.buildDestination));
|
.pipe(gulp.dest(DIST));
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('add-assets', function(){
|
||||||
|
return gulp.src('./assets/**/*').pipe(gulp.dest(DIST));
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task('webpack-build', done => {
|
gulp.task('webpack-build', done => {
|
||||||
@@ -36,40 +74,14 @@ gulp.task('webpack-build', done => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task('update-qext-version', function () {
|
|
||||||
return gulp.src(`${settings.buildDestination}/${settings.name}.qext`)
|
|
||||||
.pipe(jeditor({
|
|
||||||
'version': settings.version
|
|
||||||
}))
|
|
||||||
.pipe(gulp.dest(settings.buildDestination));
|
|
||||||
});
|
|
||||||
|
|
||||||
gulp.task('build',
|
gulp.task('build',
|
||||||
gulp.series('remove-build-folder', 'webpack-build', 'update-qext-version', 'zip-build')
|
gulp.series('clean', 'webpack-build', 'qext', 'add-assets')
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task('zip',
|
||||||
|
gulp.series('build', 'zip-build')
|
||||||
);
|
);
|
||||||
|
|
||||||
gulp.task('default',
|
gulp.task('default',
|
||||||
gulp.series('build')
|
gulp.series('build')
|
||||||
);
|
);
|
||||||
|
|
||||||
gulp.task('watch', () => new Promise((resolve, reject) => {
|
|
||||||
webpackConfig.entry.unshift('webpack-dev-server/client?http://localhost:' + settings.port);
|
|
||||||
const compiler = webpack(webpackConfig);
|
|
||||||
const originalOutputFileSystem = compiler.outputFileSystem;
|
|
||||||
const devServer = new WebpackDevServer(compiler, {
|
|
||||||
headers: {
|
|
||||||
"Access-Control-Allow-Origin": "*"
|
|
||||||
},
|
|
||||||
}).listen(settings.port, 'localhost', error => {
|
|
||||||
compiler.outputFileSystem = originalOutputFileSystem;
|
|
||||||
if (error) {
|
|
||||||
console.error(error); // eslint-disable-line no-console
|
|
||||||
return reject(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log('Listening at localhost:' + settings.port);
|
|
||||||
|
|
||||||
resolve(null, devServer);
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
const settings = require('./settings');
|
|
||||||
|
|
||||||
module.exports = (config) => {
|
|
||||||
config.set({
|
|
||||||
browsers: ['SlimChromeHeadless'],
|
|
||||||
customLaunchers: {
|
|
||||||
SlimChromeHeadless: {
|
|
||||||
base: 'ChromeHeadless',
|
|
||||||
flags: [
|
|
||||||
'--headless',
|
|
||||||
'--disable-gpu',
|
|
||||||
'--disable-translate',
|
|
||||||
'--disable-extensions'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
files: [
|
|
||||||
{
|
|
||||||
pattern: 'src/**/*.spec.js',
|
|
||||||
watched: true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
frameworks: ['jasmine'],
|
|
||||||
preprocessors: {
|
|
||||||
'src/**/*.spec.{js, jsx}': [
|
|
||||||
'webpack',
|
|
||||||
'sourcemap'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
webpack: {
|
|
||||||
devtool: 'source-map',
|
|
||||||
externals: {
|
|
||||||
jquery: {
|
|
||||||
amd: 'jquery',
|
|
||||||
commonjs: 'jquery',
|
|
||||||
commonjs2: 'jquery',
|
|
||||||
root: '_'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mode: settings.mode,
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
exclude: [/node_modules/],
|
|
||||||
loader: 'babel-loader',
|
|
||||||
options: {
|
|
||||||
plugins: [
|
|
||||||
'@babel/plugin-transform-async-to-generator',
|
|
||||||
'@babel/plugin-proposal-class-properties'],
|
|
||||||
presets: ['@babel/preset-react']
|
|
||||||
},
|
|
||||||
test: /\.(js|jsx)$/
|
|
||||||
},
|
|
||||||
{
|
|
||||||
loader: 'ignore-loader',
|
|
||||||
test: /\.less$/
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'test-utilities': path.resolve('test/test-utilities')
|
|
||||||
},
|
|
||||||
modules: ['node_modules']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
2633
package-lock.json
generated
2633
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
23
package.json
23
package.json
@@ -1,18 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "qlik-smart-pivot",
|
"name": "qlik-smart-pivot",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "Formatted table for P&L reports.",
|
"description": "Profit & Loss reporting with color and font customizations.",
|
||||||
"keywords": "smart pivot qliksense extension",
|
"homepage": "",
|
||||||
"repository": "https://github.com/iviasensio/PLSmartPivot",
|
"repository": "https://github.com/iviasensio/PLSmartPivot",
|
||||||
"author": "Ivan Felipe Asensio <ivan.felipe@qlik.com>",
|
"author": "Ivan Felipe Asensio <ivan.felipe@qlik.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "gulp build",
|
"build": "gulp build",
|
||||||
|
"build:zip": "gulp zip",
|
||||||
"eslint": "eslint src",
|
"eslint": "eslint src",
|
||||||
"eslint:fix": "eslint --fix src",
|
|
||||||
"test": "karma start karma.conf.js",
|
|
||||||
"test-once": "karma start karma.conf.js --single-run",
|
|
||||||
"watch": "gulp watch",
|
|
||||||
"stylelint": "stylelint src/main.less"
|
"stylelint": "stylelint src/main.less"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -24,7 +21,6 @@
|
|||||||
"@babel/preset-react": "7.0.0",
|
"@babel/preset-react": "7.0.0",
|
||||||
"babel-eslint": "10.0.1",
|
"babel-eslint": "10.0.1",
|
||||||
"babel-loader": "8.0.4",
|
"babel-loader": "8.0.4",
|
||||||
"copy-webpack-plugin": "4.5.3",
|
|
||||||
"css-loader": "1.0.0",
|
"css-loader": "1.0.0",
|
||||||
"del": "2.0.2",
|
"del": "2.0.2",
|
||||||
"enzyme": "3.8.0",
|
"enzyme": "3.8.0",
|
||||||
@@ -33,15 +29,8 @@
|
|||||||
"eslint-loader": "2.1.1",
|
"eslint-loader": "2.1.1",
|
||||||
"eslint-plugin-react": "7.11.1",
|
"eslint-plugin-react": "7.11.1",
|
||||||
"gulp": "4.0.0",
|
"gulp": "4.0.0",
|
||||||
"gulp-json-editor": "2.4.3",
|
"gulp-util": "^3.0.7",
|
||||||
"gulp-zip": "3.0.2",
|
"gulp-zip": "3.0.2",
|
||||||
"jasmine-core": "3.2.1",
|
|
||||||
"jasmine-enzyme": "7.0.1",
|
|
||||||
"karma": "3.0.0",
|
|
||||||
"karma-chrome-launcher": "2.2.0",
|
|
||||||
"karma-jasmine": "1.1.2",
|
|
||||||
"karma-sourcemap-loader": "0.3.7",
|
|
||||||
"karma-webpack": "3.0.5",
|
|
||||||
"less": "3.8.1",
|
"less": "3.8.1",
|
||||||
"less-loader": "4.1.0",
|
"less-loader": "4.1.0",
|
||||||
"lodash.merge": "4.6.1",
|
"lodash.merge": "4.6.1",
|
||||||
@@ -49,9 +38,7 @@
|
|||||||
"stylelint": "8.4.0",
|
"stylelint": "8.4.0",
|
||||||
"stylelint-webpack-plugin": "0.10.5",
|
"stylelint-webpack-plugin": "0.10.5",
|
||||||
"text-loader": "0.0.1",
|
"text-loader": "0.0.1",
|
||||||
"webpack": "4.20.2",
|
"webpack": "4.20.2"
|
||||||
"webpack-cli": "3.1.2",
|
|
||||||
"webpack-dev-server": "3.1.10"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prop-types": "15.6.2",
|
"prop-types": "15.6.2",
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.2 KiB |
@@ -2,8 +2,8 @@
|
|||||||
set -o errexit
|
set -o errexit
|
||||||
|
|
||||||
echo "Creating release for version: $VERSION"
|
echo "Creating release for version: $VERSION"
|
||||||
echo "Artifact name: ./build/${3}_${VERSION}.zip"
|
echo "Artifact name: ./dist/${3}_${VERSION}.zip"
|
||||||
$HOME/bin/ghr -t ${ghoauth} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} ${VERSION} "./build/${3}_${4}.zip"
|
$HOME/bin/ghr -t ${ghoauth} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} ${VERSION} "./dist/${3}_${4}.zip"
|
||||||
|
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
|
|||||||
25
scripts/verify-files.sh
Normal file
25
scripts/verify-files.sh
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# The build script has a known race-condition that sometimes causes it to not include all files
|
||||||
|
# in the built zip. This script verifies the that the zip contains the correct number of files.
|
||||||
|
|
||||||
|
set -o errexit
|
||||||
|
|
||||||
|
echo "Verifying built file count"
|
||||||
|
|
||||||
|
while read line; do
|
||||||
|
if [[ $line =~ ^\"name\": ]]; then
|
||||||
|
name=${line#*: \"}
|
||||||
|
name=${name%\"*}
|
||||||
|
fi
|
||||||
|
done < package.json
|
||||||
|
|
||||||
|
expected_file_count=$(($(find dist -type f | wc -l)-1))
|
||||||
|
zip_file_count=$(zipinfo dist/${name}_${VERSION}.zip | grep ^- | wc -l)
|
||||||
|
|
||||||
|
if [ "${expected_file_count}" -ne "${zip_file_count}" ]; then
|
||||||
|
# File count is incorrect
|
||||||
|
echo "Expected file count ${expected_file_count}, but was ${zip_file_count}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "File count OK"
|
||||||
|
exit 0
|
||||||
13
settings.js
13
settings.js
@@ -1,13 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
const packageJSON = require('./package.json');
|
|
||||||
|
|
||||||
const defaultBuildDestination = path.resolve("./build");
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
buildDestination: process.env.BUILD_PATH || defaultBuildDestination,
|
|
||||||
mode: process.env.NODE_ENV || 'development',
|
|
||||||
name: packageJSON.name,
|
|
||||||
version: process.env.VERSION || 'local-dev',
|
|
||||||
url: process.env.BUILD_URL || defaultBuildDestination,
|
|
||||||
port: process.env.PORT || 8085
|
|
||||||
};
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { ApplyPreMask } from '../masking';
|
|
||||||
import { addSeparators } from '../utilities';
|
|
||||||
import Tooltip from '../tooltip/index.jsx';
|
import Tooltip from '../tooltip/index.jsx';
|
||||||
|
|
||||||
class DataCell extends React.PureComponent {
|
class DataCell extends React.PureComponent {
|
||||||
@@ -11,78 +9,82 @@ class DataCell extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleSelect () {
|
handleSelect () {
|
||||||
const { data: { meta: { dimensionCount } }, general: { allowFilteringByClick }, measurement, qlik } = this.props;
|
const {
|
||||||
|
data: {
|
||||||
|
meta: {
|
||||||
|
dimensionCount
|
||||||
|
}
|
||||||
|
},
|
||||||
|
general: {
|
||||||
|
allowFilteringByClick
|
||||||
|
},
|
||||||
|
measurement,
|
||||||
|
component
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
const hasSecondDimension = dimensionCount > 1;
|
const hasSecondDimension = dimensionCount > 1;
|
||||||
if (!allowFilteringByClick) {
|
if (!allowFilteringByClick) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
qlik.backendApi.selectValues(0, [measurement.parents.dimension1.elementNumber], true);
|
component.backendApi.selectValues(0, [measurement.parents.dimension1.elementNumber], false);
|
||||||
|
|
||||||
if (hasSecondDimension) {
|
if (hasSecondDimension) {
|
||||||
qlik.backendApi.selectValues(1, [measurement.parents.dimension2.elementNumber], true);
|
component.backendApi.selectValues(1, [measurement.parents.dimension2.elementNumber], false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const {
|
const {
|
||||||
data,
|
cellWidth,
|
||||||
general,
|
|
||||||
measurement,
|
measurement,
|
||||||
styleBuilder,
|
styleBuilder,
|
||||||
styling
|
styling
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const isColumnPercentageBased = (/%/).test(measurement.format);
|
let textAlignment = styling.options.textAlignment || 'Right';
|
||||||
let formattedMeasurementValue = formatMeasurementValue(measurement, styling);
|
|
||||||
if (styleBuilder.hasComments()) {
|
|
||||||
formattedMeasurementValue = '.';
|
|
||||||
}
|
|
||||||
let textAlignment = 'Right';
|
|
||||||
const textAlignmentProp = styling.options.textAlignment;
|
|
||||||
if (textAlignmentProp) {
|
|
||||||
textAlignment = textAlignmentProp;
|
|
||||||
}
|
|
||||||
|
|
||||||
let cellStyle = {
|
let cellStyle = {
|
||||||
fontFamily: styling.options.fontFamily,
|
fontFamily: styling.options.fontFamily,
|
||||||
...styleBuilder.getStyle(),
|
...styleBuilder.getStyle(),
|
||||||
paddingLeft: '5px',
|
paddingLeft: '5px',
|
||||||
textAlign: textAlignment
|
textAlign: textAlignment,
|
||||||
|
minWidth: cellWidth,
|
||||||
|
maxWidth: cellWidth
|
||||||
};
|
};
|
||||||
|
|
||||||
const { semaphoreColors, semaphoreColors: { fieldsToApplyTo } } = styling;
|
const isEmptyCell = measurement.displayValue === '';
|
||||||
const isValidSemaphoreValue = !styleBuilder.hasComments() && !isNaN(measurement.value);
|
let formattedMeasurementValue;
|
||||||
const dimension1Row = measurement.parents.dimension1.elementNumber;
|
if (isEmptyCell || styleBuilder.hasComments()) {
|
||||||
const isSpecifiedMetricField = fieldsToApplyTo.metricsSpecificFields.indexOf(dimension1Row) !== -1;
|
formattedMeasurementValue = '';
|
||||||
const shouldHaveSemaphoreColors = (fieldsToApplyTo.applyToMetric || isSpecifiedMetricField);
|
cellStyle.cursor = 'default';
|
||||||
if (isValidSemaphoreValue && shouldHaveSemaphoreColors) {
|
} else {
|
||||||
const { backgroundColor, color } = getSemaphoreColors(measurement, semaphoreColors);
|
formattedMeasurementValue = formatMeasurementValue(measurement, styling);
|
||||||
cellStyle = {
|
|
||||||
...styleBuilder.getStyle(),
|
|
||||||
backgroundColor,
|
|
||||||
color,
|
|
||||||
fontFamily: styling.options.fontFamily,
|
|
||||||
paddingLeft: '5px',
|
|
||||||
textAlign: textAlignment
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let cellClass = 'grid-cells';
|
const { conditionalColoring } = styling;
|
||||||
const hasTwoDimensions = data.headers.dimension2 && data.headers.dimension2.length > 0;
|
if (conditionalColoring.enabled) {
|
||||||
const shouldUseSmallCells = isColumnPercentageBased && data.headers.measurements.length > 1 && hasTwoDimensions;
|
const isValidConditionalColoringValue = !styleBuilder.hasComments() && !isNaN(measurement.value);
|
||||||
if (shouldUseSmallCells) {
|
const isSpecifiedRow =
|
||||||
cellClass = 'grid-cells-small';
|
conditionalColoring.rows.indexOf(measurement.parents.dimension1.header) !== -1;
|
||||||
|
const isSpecifiedMeasure =
|
||||||
|
conditionalColoring.measures.indexOf(measurement.parents.measurement.index) !== -1;
|
||||||
|
const shouldHaveConditionalColoring = (conditionalColoring.colorAllRows || isSpecifiedRow)
|
||||||
|
&& (conditionalColoring.colorAllMeasures || isSpecifiedMeasure);
|
||||||
|
if (isValidConditionalColoringValue && shouldHaveConditionalColoring) {
|
||||||
|
const { color, textColor } = getConditionalColor(measurement, conditionalColoring);
|
||||||
|
cellStyle.backgroundColor = color.color;
|
||||||
|
cellStyle.color = textColor.color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<td
|
<td
|
||||||
className={`${cellClass}${general.cellSuffix}`}
|
className="grid-cells"
|
||||||
onClick={this.handleSelect}
|
onClick={isEmptyCell ? null : this.handleSelect}
|
||||||
style={cellStyle}
|
style={cellStyle}
|
||||||
>
|
>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
styling={styling}
|
||||||
tooltipText={formattedMeasurementValue}
|
tooltipText={formattedMeasurementValue}
|
||||||
>
|
>
|
||||||
{formattedMeasurementValue}
|
{formattedMeasurementValue}
|
||||||
@@ -93,20 +95,22 @@ class DataCell extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DataCell.propTypes = {
|
DataCell.propTypes = {
|
||||||
|
cellWidth: PropTypes.string.isRequired,
|
||||||
data: PropTypes.shape({
|
data: PropTypes.shape({
|
||||||
headers: PropTypes.shape({
|
headers: PropTypes.shape({
|
||||||
measurements: PropTypes.array.isRequired
|
measurements: PropTypes.array.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
meta: PropTypes.shape({
|
||||||
|
dimensionCount: PropTypes.number.isRequired
|
||||||
}).isRequired
|
}).isRequired
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
general: PropTypes.shape({
|
general: PropTypes.shape({}).isRequired,
|
||||||
cellSuffix: PropTypes.string.isRequired
|
|
||||||
}).isRequired,
|
|
||||||
measurement: PropTypes.shape({
|
measurement: PropTypes.shape({
|
||||||
format: PropTypes.string,
|
format: PropTypes.string,
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
value: PropTypes.any
|
value: PropTypes.any
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
qlik: PropTypes.shape({
|
component: PropTypes.shape({
|
||||||
backendApi: PropTypes.shape({
|
backendApi: PropTypes.shape({
|
||||||
selectValues: function (props, propName) {
|
selectValues: function (props, propName) {
|
||||||
if (props.isSnapshot || typeof props[propName] === 'function') {
|
if (props.isSnapshot || typeof props[propName] === 'function') {
|
||||||
@@ -127,59 +131,19 @@ DataCell.propTypes = {
|
|||||||
export default DataCell;
|
export default DataCell;
|
||||||
|
|
||||||
function formatMeasurementValue (measurement, styling) {
|
function formatMeasurementValue (measurement, styling) {
|
||||||
const isColumnPercentageBased = (/%/).test(measurement.format);
|
if (isNaN(measurement.value)) {
|
||||||
let formattedMeasurementValue = '';
|
return styling.symbolForNulls;
|
||||||
if (isColumnPercentageBased) {
|
|
||||||
if (isNaN(measurement.value)) {
|
|
||||||
formattedMeasurementValue = styling.symbolForNulls;
|
|
||||||
} else {
|
|
||||||
formattedMeasurementValue = ApplyPreMask('0,00%', measurement.value);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let magnitudeDivider;
|
|
||||||
switch (measurement.magnitude.toLowerCase()) {
|
|
||||||
case 'k':
|
|
||||||
magnitudeDivider = 1000;
|
|
||||||
break;
|
|
||||||
case 'm':
|
|
||||||
magnitudeDivider = 1000000;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
magnitudeDivider = 1;
|
|
||||||
}
|
|
||||||
const formattingStringWithoutMagnitude = measurement.format.replace(/k|K|m|M/gi, '');
|
|
||||||
if (isNaN(measurement.value)) {
|
|
||||||
formattedMeasurementValue = styling.symbolForNulls;
|
|
||||||
} else {
|
|
||||||
let preFormatValue = measurement.value;
|
|
||||||
if (isColumnPercentageBased) {
|
|
||||||
preFormatValue *= 100;
|
|
||||||
}
|
|
||||||
switch (formattingStringWithoutMagnitude) {
|
|
||||||
case '#.##0':
|
|
||||||
formattedMeasurementValue = addSeparators((preFormatValue / magnitudeDivider), '.', ',', 0);
|
|
||||||
break;
|
|
||||||
case '#,##0':
|
|
||||||
formattedMeasurementValue = addSeparators((preFormatValue / magnitudeDivider), ',', '.', 0);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
formattedMeasurementValue = ApplyPreMask(
|
|
||||||
formattingStringWithoutMagnitude,
|
|
||||||
(preFormatValue / magnitudeDivider)
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return formattedMeasurementValue;
|
|
||||||
|
return measurement.displayValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSemaphoreColors (measurement, semaphoreColors) {
|
function getConditionalColor (measurement, conditionalColoring) {
|
||||||
if (measurement.value < semaphoreColors.status.critical) {
|
if (measurement.value < conditionalColoring.threshold.poor) {
|
||||||
return semaphoreColors.statusColors.critical;
|
return conditionalColoring.colors.poor;
|
||||||
}
|
}
|
||||||
if (measurement.value < semaphoreColors.status.medium) {
|
if (measurement.value < conditionalColoring.threshold.fair) {
|
||||||
return semaphoreColors.statusColors.medium;
|
return conditionalColoring.colors.fair;
|
||||||
}
|
}
|
||||||
return semaphoreColors.statusColors.normal;
|
return conditionalColoring.colors.good;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,98 +5,143 @@ import DataCell from './data-cell.jsx';
|
|||||||
import RowHeader from './row-header.jsx';
|
import RowHeader from './row-header.jsx';
|
||||||
import { injectSeparators } from '../utilities';
|
import { injectSeparators } from '../utilities';
|
||||||
|
|
||||||
const DataTable = ({ data, general, qlik, renderData, styling }) => {
|
// eslint-disable-next-line react/prefer-stateless-function
|
||||||
const {
|
class DataTable extends React.PureComponent {
|
||||||
headers: {
|
render () {
|
||||||
dimension1,
|
const {
|
||||||
measurements
|
cellWidth,
|
||||||
},
|
columnSeparatorWidth,
|
||||||
matrix
|
component,
|
||||||
} = data;
|
data,
|
||||||
|
general,
|
||||||
|
renderData,
|
||||||
|
styling
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
return (
|
const {
|
||||||
<div className="row-wrapper">
|
headers: {
|
||||||
<table>
|
dimension1,
|
||||||
<tbody>
|
dimension2,
|
||||||
{dimension1.map((dimensionEntry, dimensionIndex) => {
|
measurements
|
||||||
const rowHeaderText = dimensionEntry.displayValue || '';
|
},
|
||||||
if (rowHeaderText === '-') {
|
matrix
|
||||||
return null;
|
} = data;
|
||||||
|
|
||||||
|
const separatorStyle = {
|
||||||
|
minWidth: columnSeparatorWidth,
|
||||||
|
maxWidth: columnSeparatorWidth
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderMeasurementData = (dimIndex, atEvery) => {
|
||||||
|
const injectSeparatorsArray = injectSeparators(
|
||||||
|
matrix[dimIndex],
|
||||||
|
columnSeparatorWidth,
|
||||||
|
atEvery
|
||||||
|
);
|
||||||
|
|
||||||
|
if (dimension2.length <= 0) {
|
||||||
|
return injectSeparatorsArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
let measurementDataRow = [],
|
||||||
|
index = 0;
|
||||||
|
dimension2.forEach((dim2) => {
|
||||||
|
measurements.forEach((measure) => {
|
||||||
|
for (index = 0; index < injectSeparatorsArray.length; index++) {
|
||||||
|
if (injectSeparatorsArray[index].parents && dimension1[dimIndex].displayValue === injectSeparatorsArray[index].parents.dimension1.header) {
|
||||||
|
if (dim2.displayValue === injectSeparatorsArray[index].parents.dimension2.header) {
|
||||||
|
if (measure.name === injectSeparatorsArray[index].parents.measurement.header) {
|
||||||
|
measurementDataRow.push(injectSeparatorsArray[index]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const styleBuilder = new StyleBuilder(styling);
|
}
|
||||||
if (styling.hasCustomFileStyle) {
|
});
|
||||||
styleBuilder.parseCustomFileStyle(rowHeaderText);
|
});
|
||||||
} else {
|
return measurementDataRow;
|
||||||
styleBuilder.applyStandardAttributes(dimensionIndex);
|
};
|
||||||
styleBuilder.applyCustomStyle({
|
|
||||||
fontSize: `${14 + styling.options.fontSizeAdjustment}px`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const rowStyle = {
|
|
||||||
fontFamily: styling.options.fontFamily,
|
|
||||||
width: '230px',
|
|
||||||
...styleBuilder.getStyle()
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={dimensionEntry.displayValue}>
|
<div className="row-wrapper">
|
||||||
<RowHeader
|
<table>
|
||||||
entry={dimensionEntry}
|
<tbody>
|
||||||
qlik={qlik}
|
{dimension1.map((dimensionEntry, dimensionIndex) => {
|
||||||
rowStyle={rowStyle}
|
const rowHeaderText = dimensionEntry.displayValue || '';
|
||||||
styleBuilder={styleBuilder}
|
if (rowHeaderText === '-') {
|
||||||
styling={styling}
|
return null;
|
||||||
/>
|
}
|
||||||
{renderData && injectSeparators(
|
const styleBuilder = new StyleBuilder(styling);
|
||||||
matrix[dimensionIndex],
|
if (styling.hasCustomFileStyle) {
|
||||||
styling.useSeparatorColumns,
|
styleBuilder.parseCustomFileStyle(rowHeaderText);
|
||||||
{ atEvery: measurements.length }
|
} else {
|
||||||
).map((measurementData, index) => {
|
styleBuilder.applyStandardAttributes(dimensionIndex);
|
||||||
if (measurementData.isSeparator) {
|
styleBuilder.applyCustomStyle({
|
||||||
const separatorStyle = {
|
fontSize: `${14 + styling.options.fontSizeAdjustment}px`
|
||||||
color: 'white',
|
});
|
||||||
fontFamily: styling.options.fontFamily,
|
}
|
||||||
fontSize: `${12 + styling.options.fontSizeAdjustment}px`
|
const rowStyle = {
|
||||||
};
|
fontFamily: styling.options.fontFamily,
|
||||||
|
width: '230px',
|
||||||
|
...styleBuilder.getStyle()
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<td
|
<tr key={dimensionEntry.displayValue}>
|
||||||
className="empty"
|
{!renderData ?
|
||||||
key={`${dimensionEntry.displayValue}-${index}-separator`}
|
<RowHeader
|
||||||
style={separatorStyle}
|
component={component}
|
||||||
>
|
entry={dimensionEntry}
|
||||||
*
|
rowStyle={rowStyle}
|
||||||
</td>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const { dimension1: dimension1Info, dimension2, measurement } = measurementData.parents;
|
|
||||||
const id = `${dimension1Info.elementNumber}-${dimension2 && dimension2.elementNumber}-${measurement.header}`;
|
|
||||||
return (
|
|
||||||
<DataCell
|
|
||||||
data={data}
|
|
||||||
general={general}
|
|
||||||
key={`${dimensionEntry.displayValue}-${id}`}
|
|
||||||
measurement={measurementData}
|
|
||||||
qlik={qlik}
|
|
||||||
styleBuilder={styleBuilder}
|
styleBuilder={styleBuilder}
|
||||||
styling={styling}
|
styling={styling}
|
||||||
/>
|
/> : null
|
||||||
);
|
}
|
||||||
})}
|
{renderData && renderMeasurementData(dimensionIndex, { atEvery: measurements.length }).map((measurementData, index) => {
|
||||||
</tr>
|
if (measurementData.isSeparator) {
|
||||||
);
|
return (
|
||||||
})}
|
<td
|
||||||
</tbody>
|
className="empty"
|
||||||
</table>
|
key={`${dimensionEntry.displayValue}-${index}-separator`}
|
||||||
</div>
|
style={separatorStyle}
|
||||||
);
|
/>
|
||||||
};
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-shadow
|
||||||
|
const { dimension1: dimension1Info, dimension2, measurement } = measurementData.parents;
|
||||||
|
const id = `${dimension1Info.elementNumber}-${dimension2 && dimension2.elementNumber}-${measurement.header}-${measurement.index}`;
|
||||||
|
return (
|
||||||
|
<DataCell
|
||||||
|
cellWidth={cellWidth}
|
||||||
|
component={component}
|
||||||
|
data={data}
|
||||||
|
general={general}
|
||||||
|
key={`${dimensionEntry.displayValue}-${id}`}
|
||||||
|
measurement={measurementData}
|
||||||
|
styleBuilder={styleBuilder}
|
||||||
|
styling={styling}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DataTable.defaultProps = {
|
DataTable.defaultProps = {
|
||||||
renderData: true
|
renderData: true
|
||||||
};
|
};
|
||||||
|
|
||||||
DataTable.propTypes = {
|
DataTable.propTypes = {
|
||||||
|
cellWidth: PropTypes.string.isRequired,
|
||||||
|
columnSeparatorWidth: PropTypes.string.isRequired,
|
||||||
|
component: PropTypes.shape({}).isRequired,
|
||||||
data: PropTypes.shape({
|
data: PropTypes.shape({
|
||||||
headers: PropTypes.shape({
|
headers: PropTypes.shape({
|
||||||
dimension1: PropTypes.array.isRequired
|
dimension1: PropTypes.array.isRequired
|
||||||
@@ -104,7 +149,6 @@ DataTable.propTypes = {
|
|||||||
matrix: PropTypes.arrayOf(PropTypes.array.isRequired).isRequired
|
matrix: PropTypes.arrayOf(PropTypes.array.isRequired).isRequired
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
general: PropTypes.shape({}).isRequired,
|
general: PropTypes.shape({}).isRequired,
|
||||||
qlik: PropTypes.shape({}).isRequired,
|
|
||||||
renderData: PropTypes.bool,
|
renderData: PropTypes.bool,
|
||||||
styling: PropTypes.shape({
|
styling: PropTypes.shape({
|
||||||
hasCustomFileStyle: PropTypes.bool.isRequired
|
hasCustomFileStyle: PropTypes.bool.isRequired
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ class RowHeader extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleSelect () {
|
handleSelect () {
|
||||||
const { entry, qlik } = this.props;
|
const { component, entry } = this.props;
|
||||||
qlik.backendApi.selectValues(0, [entry.elementNumber], true);
|
component.backendApi.selectValues(0, [entry.elementNumber], false);
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { entry, rowStyle, styleBuilder, styling, qlik } = this.props;
|
const { entry, rowStyle, styleBuilder, styling, component } = this.props;
|
||||||
const inEditState = qlik.inEditState();
|
const inEditState = component.inEditState();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<td
|
<td
|
||||||
@@ -27,6 +27,7 @@ class RowHeader extends React.PureComponent {
|
|||||||
>
|
>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
isTooltipActive={!inEditState}
|
isTooltipActive={!inEditState}
|
||||||
|
styling={styling}
|
||||||
tooltipText={entry.displayValue}
|
tooltipText={entry.displayValue}
|
||||||
>
|
>
|
||||||
<HeaderPadding
|
<HeaderPadding
|
||||||
@@ -42,9 +43,10 @@ class RowHeader extends React.PureComponent {
|
|||||||
|
|
||||||
RowHeader.propTypes = {
|
RowHeader.propTypes = {
|
||||||
entry: PropTypes.shape({
|
entry: PropTypes.shape({
|
||||||
displayValue: PropTypes.string.isRequired
|
displayValue: PropTypes.string.isRequired,
|
||||||
|
elementNumber: PropTypes.number.isRequired
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
qlik: PropTypes.shape({
|
component: PropTypes.shape({
|
||||||
backendApi: PropTypes.shape({
|
backendApi: PropTypes.shape({
|
||||||
selectValues: function (props, propName) {
|
selectValues: function (props, propName) {
|
||||||
if (props.isSnapshot || typeof props[propName] === 'function') {
|
if (props.isSnapshot || typeof props[propName] === 'function') {
|
||||||
|
|||||||
132
src/dataset.js
132
src/dataset.js
@@ -6,86 +6,86 @@ function createCube (definition, app) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildDataCube (originCubeDefinition, dimensionIndexes, app) {
|
async function buildDataCube (originCubeDefinition, originCube, app, requestPage) {
|
||||||
const cubeDefinition = {
|
const cubeDefinition = {
|
||||||
...originCubeDefinition,
|
...originCubeDefinition,
|
||||||
qInitialDataFetch: [
|
qInitialDataFetch: [
|
||||||
{
|
{
|
||||||
|
// eslint-disable-next-line no-undefined
|
||||||
|
qTop: requestPage === undefined ? 0 : requestPage[0].qTop,
|
||||||
|
qLeft: 0,
|
||||||
qHeight: 1000,
|
qHeight: 1000,
|
||||||
qWidth: 10
|
qWidth: originCube.qSize.qcx
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
qDimensions: [originCubeDefinition.qDimensions[dimensionIndexes.dimension1]],
|
qDimensions: [originCubeDefinition.qDimensions[0]],
|
||||||
qMeasures: originCubeDefinition.qMeasures
|
qMeasures: originCubeDefinition.qMeasures
|
||||||
};
|
};
|
||||||
if (dimensionIndexes.dimension2) {
|
if (originCube.qDimensionInfo.length === 2) {
|
||||||
cubeDefinition.qDimensions.push(originCubeDefinition.qDimensions[dimensionIndexes.dimension2]);
|
cubeDefinition.qDimensions.push(originCubeDefinition.qDimensions[1]);
|
||||||
}
|
}
|
||||||
const cube = await createCube(cubeDefinition, app);
|
const cube = await createCube(cubeDefinition, app);
|
||||||
|
const cubeMatrix = cube.qHyperCube.qDataPages[0].qMatrix;
|
||||||
return cube.qHyperCube.qDataPages[0].qMatrix;
|
app.destroySessionObject(cube.qInfo.qId);
|
||||||
|
return cubeMatrix;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildDesignCube (originCubeDefinition, dimensionIndexes, app) {
|
export async function initializeDataCube (component, layout) {
|
||||||
if (!dimensionIndexes.design) {
|
|
||||||
|
if (component.backendApi.isSnapshot) {
|
||||||
|
return layout.snapshotData.dataCube;
|
||||||
|
}
|
||||||
|
const app = qlik.currApp(component);
|
||||||
|
const properties = (await component.backendApi.getProperties());
|
||||||
|
const rowCount = component.backendApi.getRowCount();
|
||||||
|
const cellCount = rowCount * layout.qHyperCube.qSize.qcx;
|
||||||
|
const maxLoops = layout.maxloops;
|
||||||
|
|
||||||
|
// If this is a master object, fetch the hyperCubeDef of the original object
|
||||||
|
let hyperCubeDef = properties.qExtendsId
|
||||||
|
? (await app.getObjectProperties(properties.qExtendsId)).properties.qHyperCubeDef
|
||||||
|
: properties.qHyperCubeDef;
|
||||||
|
hyperCubeDef = JSON.parse(JSON.stringify(hyperCubeDef));
|
||||||
|
hyperCubeDef.qStateName = layout.qStateName;
|
||||||
|
const pagedCube = {};
|
||||||
|
let lastRow = 0;
|
||||||
|
if (cellCount < (maxLoops * 10000)) {
|
||||||
|
for (let index = 0; cellCount > lastRow; index += 1) {
|
||||||
|
const requestPage = [
|
||||||
|
{
|
||||||
|
qHeight: 1000,
|
||||||
|
qLeft: 0,
|
||||||
|
qTop: lastRow,
|
||||||
|
qWidth: 10 // should be # of columns
|
||||||
|
}
|
||||||
|
];
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
pagedCube[index] = await buildDataCube(hyperCubeDef, layout.qHyperCube, app, requestPage);
|
||||||
|
lastRow = lastRow + 1000;
|
||||||
|
}
|
||||||
|
return pagedCube;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initializeDesignList (component, layout) {
|
||||||
|
if (component.backendApi.isSnapshot) {
|
||||||
|
return layout.snapshotData.designList;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!layout.stylingfield) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const cube = await createCube({
|
|
||||||
qInitialDataFetch: [
|
|
||||||
{
|
|
||||||
qHeight: 1000,
|
|
||||||
qWidth: 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
qDimensions: [originCubeDefinition.qDimensions[dimensionIndexes.design]]
|
|
||||||
}, app);
|
|
||||||
|
|
||||||
return cube.qHyperCube.qDataPages[0].qMatrix;
|
return new Promise(resolve => {
|
||||||
}
|
const app = qlik.currApp(component);
|
||||||
|
const stylingField = app.field(layout.stylingfield);
|
||||||
const STYLE_SEPARATOR_COUNT = 7;
|
const listener = function () {
|
||||||
function findDesignDimension (qMatrix) {
|
const data = stylingField.rows.map(row => row.qText);
|
||||||
return qMatrix[0].map(entry => (entry.qText.match(/;/g) || []).length).indexOf(STYLE_SEPARATOR_COUNT);
|
stylingField.OnData.unbind(listener);
|
||||||
}
|
resolve(data);
|
||||||
|
};
|
||||||
function getDimensionIndexes (dimensionsInformation, designDimensionIndex) {
|
stylingField.OnData.bind(listener);
|
||||||
const hasDesign = designDimensionIndex !== -1;
|
stylingField.getData();
|
||||||
const nonDesignDimensionCount = hasDesign ? dimensionsInformation.length - 1 : dimensionsInformation.length;
|
});
|
||||||
const dimension1 = designDimensionIndex === 0 ? 1 : 0;
|
|
||||||
let dimension2 = false;
|
|
||||||
if (nonDesignDimensionCount === 2) {
|
|
||||||
dimension2 = hasDesign && designDimensionIndex < 2 ? 2 : 1;
|
|
||||||
}
|
|
||||||
const design = hasDesign && designDimensionIndex;
|
|
||||||
const firstMeasurementIndex = dimensionsInformation.length;
|
|
||||||
return {
|
|
||||||
design,
|
|
||||||
dimension1,
|
|
||||||
dimension2,
|
|
||||||
firstMeasurementIndex
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function initializeCubes ({ component, layout }) {
|
|
||||||
const app = qlik.currApp(component);
|
|
||||||
const designDimensionIndex = findDesignDimension(layout.qHyperCube.qDataPages[0].qMatrix);
|
|
||||||
const dimensionsInformation = layout.qHyperCube.qDimensionInfo;
|
|
||||||
const dimensionIndexes = getDimensionIndexes(dimensionsInformation, designDimensionIndex);
|
|
||||||
|
|
||||||
let properties;
|
|
||||||
if (component.backendApi.isSnapshot) {
|
|
||||||
// Fetch properties of source
|
|
||||||
properties = (await app.getObjectProperties(layout.sourceObjectId)).properties;
|
|
||||||
} else {
|
|
||||||
properties = await component.backendApi.getProperties();
|
|
||||||
}
|
|
||||||
|
|
||||||
const originCubeDefinition = properties.qHyperCubeDef;
|
|
||||||
const designCube = await buildDesignCube(originCubeDefinition, dimensionIndexes, app);
|
|
||||||
const dataCube = await buildDataCube(originCubeDefinition, dimensionIndexes, app);
|
|
||||||
|
|
||||||
return {
|
|
||||||
design: designCube,
|
|
||||||
data: dataCube
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
const conceptSemaphores = {
|
|
||||||
items: {
|
|
||||||
AllConcepts: {
|
|
||||||
component: 'switch',
|
|
||||||
defaultValue: true,
|
|
||||||
label: 'All concepts affected',
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
label: 'On',
|
|
||||||
value: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Off',
|
|
||||||
value: false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
ref: 'allsemaphores',
|
|
||||||
type: 'boolean'
|
|
||||||
},
|
|
||||||
ConceptsAffected1: {
|
|
||||||
defaultValue: '',
|
|
||||||
ref: 'conceptsemaphore1',
|
|
||||||
show: data => !data.allsemaphores,
|
|
||||||
translation: 'Concept 1',
|
|
||||||
type: 'string'
|
|
||||||
},
|
|
||||||
ConceptsAffected2: {
|
|
||||||
defaultValue: '',
|
|
||||||
ref: 'conceptsemaphore2',
|
|
||||||
show: data => !data.allsemaphores,
|
|
||||||
translation: 'Concept 2',
|
|
||||||
type: 'string'
|
|
||||||
},
|
|
||||||
ConceptsAffected3: {
|
|
||||||
defaultValue: '',
|
|
||||||
ref: 'conceptsemaphore3',
|
|
||||||
show: data => !data.allsemaphores,
|
|
||||||
translation: 'Concept 3',
|
|
||||||
type: 'string'
|
|
||||||
},
|
|
||||||
ConceptsAffected4: {
|
|
||||||
defaultValue: '',
|
|
||||||
ref: 'conceptsemaphore4',
|
|
||||||
show: data => !data.allsemaphores,
|
|
||||||
translation: 'Concept 4',
|
|
||||||
type: 'string'
|
|
||||||
},
|
|
||||||
ConceptsAffected5: {
|
|
||||||
defaultValue: '',
|
|
||||||
ref: 'conceptsemaphore5',
|
|
||||||
show: data => !data.allsemaphores,
|
|
||||||
translation: 'Concept 5',
|
|
||||||
type: 'string'
|
|
||||||
},
|
|
||||||
ConceptsAffected6: {
|
|
||||||
defaultValue: '',
|
|
||||||
ref: 'conceptsemaphore6',
|
|
||||||
show: data => !data.allsemaphores,
|
|
||||||
translation: 'Concept 6',
|
|
||||||
type: 'string'
|
|
||||||
},
|
|
||||||
ConceptsAffected7: {
|
|
||||||
defaultValue: '',
|
|
||||||
ref: 'conceptsemaphore7',
|
|
||||||
show: data => !data.allsemaphores,
|
|
||||||
translation: 'Concept 7',
|
|
||||||
type: 'string'
|
|
||||||
},
|
|
||||||
ConceptsAffected8: {
|
|
||||||
defaultValue: '',
|
|
||||||
ref: 'conceptsemaphore8',
|
|
||||||
show: data => !data.allsemaphores,
|
|
||||||
translation: 'Concept 8',
|
|
||||||
type: 'string'
|
|
||||||
},
|
|
||||||
ConceptsAffected9: {
|
|
||||||
defaultValue: '',
|
|
||||||
ref: 'conceptsemaphore9',
|
|
||||||
show: data => !data.allsemaphores,
|
|
||||||
translation: 'Concept 9',
|
|
||||||
type: 'string'
|
|
||||||
},
|
|
||||||
// eslint-disable-next-line sort-keys
|
|
||||||
ConceptsAffected10: {
|
|
||||||
defaultValue: '',
|
|
||||||
ref: 'conceptsemaphore10',
|
|
||||||
show: data => !data.allsemaphores,
|
|
||||||
translation: 'Concept 10',
|
|
||||||
type: 'string'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
label: 'Concept Semaphores',
|
|
||||||
type: 'items'
|
|
||||||
};
|
|
||||||
|
|
||||||
export default conceptSemaphores;
|
|
||||||
199
src/definition/conditional-coloring.js
Normal file
199
src/definition/conditional-coloring.js
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
const conditionalColoring = {
|
||||||
|
type: 'items',
|
||||||
|
label: 'Color by performance',
|
||||||
|
items: {
|
||||||
|
Enabled: {
|
||||||
|
ref: 'conditionalcoloring.enabled',
|
||||||
|
type: 'boolean',
|
||||||
|
label: 'Enabled',
|
||||||
|
component: 'switch',
|
||||||
|
defaultValue: false,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: true,
|
||||||
|
label: 'On'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: false,
|
||||||
|
label: 'Off'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
ColorAllRows: {
|
||||||
|
ref: 'conditionalcoloring.colorall',
|
||||||
|
type: 'boolean',
|
||||||
|
label: 'Color all rows',
|
||||||
|
component: 'switch',
|
||||||
|
defaultValue: true,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: true,
|
||||||
|
label: 'All rows'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: false,
|
||||||
|
label: 'Specified rows'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
show (data) {
|
||||||
|
return data.conditionalcoloring.enabled;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Rows: {
|
||||||
|
type: 'array',
|
||||||
|
ref: 'conditionalcoloring.rows',
|
||||||
|
label: 'Rows to color',
|
||||||
|
itemTitleRef: function (data) {
|
||||||
|
return data.rowname;
|
||||||
|
},
|
||||||
|
allowAdd: true,
|
||||||
|
allowRemove: true,
|
||||||
|
addTranslation: 'Add row to color',
|
||||||
|
items: {
|
||||||
|
Row: {
|
||||||
|
ref: 'rowname',
|
||||||
|
label: 'Name of row',
|
||||||
|
type: 'string',
|
||||||
|
defaultValue: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
show (data) {
|
||||||
|
return data.conditionalcoloring.enabled && !data.conditionalcoloring.colorall;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ColorAllMeasures: {
|
||||||
|
ref: 'conditionalcoloring.colorallmeasures',
|
||||||
|
type: 'boolean',
|
||||||
|
label: 'Color all measures',
|
||||||
|
component: 'switch',
|
||||||
|
defaultValue: true,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: true,
|
||||||
|
label: 'All measures'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: false,
|
||||||
|
label: 'Specified measures'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
show (data) {
|
||||||
|
return data.conditionalcoloring.enabled;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Measures: {
|
||||||
|
ref: 'conditionalcoloring.measures',
|
||||||
|
translation: 'Measures by index (ex: 0,3)',
|
||||||
|
type: 'string',
|
||||||
|
defaultValue: '',
|
||||||
|
show (data) {
|
||||||
|
return data.conditionalcoloring.enabled
|
||||||
|
&& data.conditionalcoloring.colorallmeasures === false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ThresholdPoor: {
|
||||||
|
ref: 'conditionalcoloring.threshold_poor',
|
||||||
|
translation: 'Poor range limit',
|
||||||
|
type: 'number',
|
||||||
|
defaultValue: -0.1,
|
||||||
|
show (data) {
|
||||||
|
return data.conditionalcoloring.enabled;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ColorPoor: {
|
||||||
|
ref: 'conditionalcoloring.color_poor',
|
||||||
|
label: 'Poor background color',
|
||||||
|
type: 'object',
|
||||||
|
component: 'color-picker',
|
||||||
|
dualOutput: true,
|
||||||
|
defaultValue: {
|
||||||
|
index: 10,
|
||||||
|
color: '#f93f17'
|
||||||
|
},
|
||||||
|
show (data) {
|
||||||
|
return data.conditionalcoloring.enabled;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
TextColorPoor: {
|
||||||
|
ref: 'conditionalcoloring.textcolor_poor',
|
||||||
|
label: 'Poor text color',
|
||||||
|
type: 'object',
|
||||||
|
component: 'color-picker',
|
||||||
|
dualOutput: true,
|
||||||
|
defaultValue: {
|
||||||
|
index: 1,
|
||||||
|
color: '#ffffff'
|
||||||
|
},
|
||||||
|
show (data) {
|
||||||
|
return data.conditionalcoloring.enabled;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ThresholdFair: {
|
||||||
|
ref: 'conditionalcoloring.threshold_fair',
|
||||||
|
translation: 'Fair range limit',
|
||||||
|
type: 'number',
|
||||||
|
defaultValue: 0,
|
||||||
|
show (data) {
|
||||||
|
return data.conditionalcoloring.enabled;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ColorFair: {
|
||||||
|
ref: 'conditionalcoloring.color_fair',
|
||||||
|
label: 'Fair background color',
|
||||||
|
type: 'object',
|
||||||
|
component: 'color-picker',
|
||||||
|
dualOutput: true,
|
||||||
|
defaultValue: {
|
||||||
|
index: 8,
|
||||||
|
color: '#ffcf02'
|
||||||
|
},
|
||||||
|
show (data) {
|
||||||
|
return data.conditionalcoloring.enabled;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
TextColorFair: {
|
||||||
|
ref: 'conditionalcoloring.textcolor_fair',
|
||||||
|
label: 'Fair text color',
|
||||||
|
type: 'object',
|
||||||
|
component: 'color-picker',
|
||||||
|
dualOutput: true,
|
||||||
|
defaultValue: {
|
||||||
|
index: 15,
|
||||||
|
color: '#000000'
|
||||||
|
},
|
||||||
|
show (data) {
|
||||||
|
return data.conditionalcoloring.enabled;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ColorGood: {
|
||||||
|
ref: 'conditionalcoloring.color_good',
|
||||||
|
label: 'Good background color',
|
||||||
|
type: 'object',
|
||||||
|
component: 'color-picker',
|
||||||
|
dualOutput: true,
|
||||||
|
defaultValue: {
|
||||||
|
index: 3,
|
||||||
|
color: '#276e27'
|
||||||
|
},
|
||||||
|
show (data) {
|
||||||
|
return data.conditionalcoloring.enabled;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
TextColorGood: {
|
||||||
|
ref: 'conditionalcoloring.textcolor_good',
|
||||||
|
label: 'Good text color',
|
||||||
|
type: 'object',
|
||||||
|
component: 'color-picker',
|
||||||
|
dualOutput: true,
|
||||||
|
defaultValue: {
|
||||||
|
index: 1,
|
||||||
|
color: '#ffffff'
|
||||||
|
},
|
||||||
|
show (data) {
|
||||||
|
return data.conditionalcoloring.enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default conditionalColoring;
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
const header = {
|
const header = {
|
||||||
type: 'items',
|
type: 'items',
|
||||||
label: 'Header Format',
|
label: 'Header format',
|
||||||
items: {
|
items: {
|
||||||
Align: {
|
Align: {
|
||||||
ref: 'HeaderAlign',
|
ref: 'HeaderAlign',
|
||||||
translation: 'Header Alignment',
|
translation: 'Header alignment',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
component: 'buttongroup',
|
component: 'buttongroup',
|
||||||
options: [
|
options: [
|
||||||
@@ -29,14 +29,14 @@ const header = {
|
|||||||
index: 6,
|
index: 6,
|
||||||
color: '#4477aa'
|
color: '#4477aa'
|
||||||
},
|
},
|
||||||
label: 'Background Header Color',
|
label: 'Background color',
|
||||||
ref: 'HeaderColorSchema',
|
ref: 'HeaderColorSchema',
|
||||||
type: 'object',
|
type: 'object',
|
||||||
dualOutput: true
|
dualOutput: true
|
||||||
},
|
},
|
||||||
HeaderTextColor: {
|
HeaderTextColor: {
|
||||||
ref: 'HeaderTextColorSchema',
|
ref: 'HeaderTextColorSchema',
|
||||||
label: 'Text Header Color',
|
label: 'Text color',
|
||||||
component: 'color-picker',
|
component: 'color-picker',
|
||||||
defaultValue: {
|
defaultValue: {
|
||||||
index: 1,
|
index: 1,
|
||||||
@@ -47,7 +47,7 @@ const header = {
|
|||||||
},
|
},
|
||||||
HeaderFontSize: {
|
HeaderFontSize: {
|
||||||
ref: 'lettersizeheader',
|
ref: 'lettersizeheader',
|
||||||
translation: 'Font Size',
|
translation: 'Font size',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
component: 'buttongroup',
|
component: 'buttongroup',
|
||||||
options: [
|
options: [
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import pagination from './pagination';
|
import pagination from './pagination';
|
||||||
import header from './header';
|
import header from './header';
|
||||||
import tableFormat from './table-format';
|
import tableFormat from './table-format';
|
||||||
import conceptSemaphores from './concept-semaphores';
|
import conditionalColoring from './conditional-coloring';
|
||||||
import metricSemaphores from './metric-semaphores';
|
|
||||||
|
|
||||||
const definition = {
|
const definition = {
|
||||||
component: 'accordion',
|
component: 'accordion',
|
||||||
@@ -18,18 +17,37 @@ const definition = {
|
|||||||
},
|
},
|
||||||
uses: 'data'
|
uses: 'data'
|
||||||
},
|
},
|
||||||
|
sorting: {
|
||||||
|
uses: 'sorting'
|
||||||
|
},
|
||||||
settings: {
|
settings: {
|
||||||
items: {
|
items: {
|
||||||
ConceptSemaphores: conceptSemaphores,
|
|
||||||
Formatted: tableFormat,
|
Formatted: tableFormat,
|
||||||
Header: header,
|
Header: header,
|
||||||
MetricSemaphores: metricSemaphores,
|
ConditionalColoring: conditionalColoring,
|
||||||
Pagination: pagination
|
Pagination: pagination
|
||||||
},
|
},
|
||||||
uses: 'settings'
|
uses: 'settings'
|
||||||
},
|
},
|
||||||
sorting: {
|
about: {
|
||||||
uses: 'sorting'
|
component: 'items',
|
||||||
|
label: 'About',
|
||||||
|
items: {
|
||||||
|
header: {
|
||||||
|
label: 'P&L pivot',
|
||||||
|
style: 'header',
|
||||||
|
component: 'text'
|
||||||
|
},
|
||||||
|
paragraph1: {
|
||||||
|
label: `P&L pivot is a Qlik Sense chart which allows you to display Profit & Loss
|
||||||
|
reporting with color and font customizations.`,
|
||||||
|
component: 'text'
|
||||||
|
},
|
||||||
|
paragraph2: {
|
||||||
|
label: 'P&L pivot is based upon an extension created by Ivan Felipe Asensio.',
|
||||||
|
component: 'text'
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
type: 'items'
|
type: 'items'
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
const metricSemaphores = {
|
|
||||||
type: 'items',
|
|
||||||
label: 'Metric Semaphores',
|
|
||||||
items: {
|
|
||||||
AllMetrics: {
|
|
||||||
ref: 'allmetrics',
|
|
||||||
type: 'boolean',
|
|
||||||
component: 'switch',
|
|
||||||
label: 'All metrics affected',
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
value: true,
|
|
||||||
label: 'On'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: false,
|
|
||||||
label: 'Off'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
defaultValue: false
|
|
||||||
},
|
|
||||||
MetricsAffected: {
|
|
||||||
ref: 'metricssemaphore',
|
|
||||||
translation: 'Metrics affected (1,2,4,...)',
|
|
||||||
type: 'string',
|
|
||||||
defaultValue: '-',
|
|
||||||
show (data) {
|
|
||||||
return !data.allmetrics;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
MetricStatus1: {
|
|
||||||
ref: 'metricsstatus1',
|
|
||||||
translation: 'Critic is less than',
|
|
||||||
type: 'number',
|
|
||||||
defaultValue: -0.1
|
|
||||||
},
|
|
||||||
ColorStatus1: {
|
|
||||||
ref: 'colorstatus1',
|
|
||||||
label: 'Critic Color Fill',
|
|
||||||
type: 'object',
|
|
||||||
component: 'color-picker',
|
|
||||||
defaultValue: {
|
|
||||||
index: 8,
|
|
||||||
color: '#f93f17'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ColorStatus1Text: {
|
|
||||||
ref: 'colorstatus1text',
|
|
||||||
label: 'Critic Color Text',
|
|
||||||
type: 'object',
|
|
||||||
component: 'color-picker',
|
|
||||||
defaultValue: {
|
|
||||||
index: 11,
|
|
||||||
color: '#ffffff'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
MetricStatus2: {
|
|
||||||
ref: 'metricsstatus2',
|
|
||||||
translation: 'Medium is less than',
|
|
||||||
type: 'number',
|
|
||||||
defaultValue: 0
|
|
||||||
},
|
|
||||||
ColorStatus2: {
|
|
||||||
ref: 'colorstatus2',
|
|
||||||
label: 'Medium Color Fill',
|
|
||||||
type: 'object',
|
|
||||||
component: 'color-picker',
|
|
||||||
defaultValue: {
|
|
||||||
index: 9,
|
|
||||||
color: '#ffcf02'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ColorStatus2Text: {
|
|
||||||
ref: 'colorstatus2text',
|
|
||||||
label: 'Medium Color Text',
|
|
||||||
type: 'object',
|
|
||||||
component: 'color-picker',
|
|
||||||
defaultValue: {
|
|
||||||
index: 12,
|
|
||||||
color: '#000000'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ColorStatus3: {
|
|
||||||
ref: 'colorstatus3',
|
|
||||||
label: 'Success Color Fill',
|
|
||||||
type: 'object',
|
|
||||||
component: 'color-picker',
|
|
||||||
defaultValue: {
|
|
||||||
index: 10,
|
|
||||||
color: '#276e27'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ColorStatus3Text: {
|
|
||||||
ref: 'colorstatus3text',
|
|
||||||
label: 'Success Color Text',
|
|
||||||
type: 'object',
|
|
||||||
component: 'color-picker',
|
|
||||||
defaultValue: {
|
|
||||||
index: 11,
|
|
||||||
color: '#ffffff'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default metricSemaphores;
|
|
||||||
@@ -6,7 +6,7 @@ const pagination = {
|
|||||||
ref: 'maxloops',
|
ref: 'maxloops',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
component: 'dropdown',
|
component: 'dropdown',
|
||||||
label: 'Max Pagination Loops',
|
label: 'Max pagination loops',
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
value: 1,
|
value: 1,
|
||||||
@@ -23,30 +23,6 @@ const pagination = {
|
|||||||
{
|
{
|
||||||
value: 4,
|
value: 4,
|
||||||
label: '40k cells'
|
label: '40k cells'
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 5,
|
|
||||||
label: '50k cells'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 6,
|
|
||||||
label: '60k cells'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 7,
|
|
||||||
label: '70k cells'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 8,
|
|
||||||
label: '80k cells'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 9,
|
|
||||||
label: '90k cells'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 10,
|
|
||||||
label: '100k cells'
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
defaultValue: 2
|
defaultValue: 2
|
||||||
@@ -55,7 +31,8 @@ const pagination = {
|
|||||||
ref: 'errormessage',
|
ref: 'errormessage',
|
||||||
label: 'Default error message',
|
label: 'Default error message',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
defaultValue: 'Ups! It seems you asked for too many data. Please filter more to see the whole picture.'
|
defaultValue: `Unable to display all the data.
|
||||||
|
Change the pagination size supported or apply more filters to limit the amount of displayed data.`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,53 @@
|
|||||||
|
const qlik = window.require('qlik');
|
||||||
|
|
||||||
// fixes case for when there are 3 dimensions, missies the case with 1 design dimension and 1 data dimension
|
// fixes case for when there are 3 dimensions, missies the case with 1 design dimension and 1 data dimension
|
||||||
function hasDesignDimension (data) {
|
function hasDesignDimension (data) {
|
||||||
return data.qHyperCubeDef.qDimensions.length > 2;
|
return data.qHyperCubeDef.qDimensions.length > 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getFieldList () {
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
const app = qlik.currApp();
|
||||||
|
app.getList('FieldList').then(function (model) {
|
||||||
|
// Close the model to prevent any updates.
|
||||||
|
app.destroySessionObject(model.layout.qInfo.qId);
|
||||||
|
|
||||||
|
// This is a bit iffy, might be smarter to reject and handle empty lists on the props instead.
|
||||||
|
if (!model.layout.qFieldList.qItems) {
|
||||||
|
return resolve([]);
|
||||||
|
}
|
||||||
|
// Resolve an array with master objects.
|
||||||
|
return resolve(model.layout.qFieldList.qItems.map(function (item) {
|
||||||
|
return {
|
||||||
|
value: item.qName,
|
||||||
|
label: item.qName
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const tableFormat = {
|
const tableFormat = {
|
||||||
type: 'items',
|
type: 'items',
|
||||||
label: 'Table Format',
|
label: 'Table format',
|
||||||
items: {
|
items: {
|
||||||
|
StylingField: {
|
||||||
|
ref: 'stylingfield',
|
||||||
|
disabledRef: '',
|
||||||
|
type: 'string',
|
||||||
|
component: 'dropdown',
|
||||||
|
label: 'Style template field',
|
||||||
|
options: function () {
|
||||||
|
return getFieldList().then(function (items) {
|
||||||
|
items.unshift(
|
||||||
|
{
|
||||||
|
value: '',
|
||||||
|
label: 'None'
|
||||||
|
});
|
||||||
|
return items;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
IndentBool: {
|
IndentBool: {
|
||||||
ref: 'indentbool',
|
ref: 'indentbool',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
@@ -17,7 +58,7 @@ const tableFormat = {
|
|||||||
SeparatorColumns: {
|
SeparatorColumns: {
|
||||||
ref: 'separatorcols',
|
ref: 'separatorcols',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
label: 'Separator Columns',
|
label: 'Column separators',
|
||||||
defaultValue: false
|
defaultValue: false
|
||||||
},
|
},
|
||||||
rowEvenBGColor: {
|
rowEvenBGColor: {
|
||||||
@@ -48,7 +89,7 @@ const tableFormat = {
|
|||||||
ref: 'BodyTextColorSchema',
|
ref: 'BodyTextColorSchema',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
component: 'dropdown',
|
component: 'dropdown',
|
||||||
label: 'Text Body Color',
|
label: 'Text body color',
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
value: 'Black',
|
value: 'Black',
|
||||||
@@ -98,7 +139,7 @@ const tableFormat = {
|
|||||||
ref: 'FontFamily',
|
ref: 'FontFamily',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
component: 'dropdown',
|
component: 'dropdown',
|
||||||
label: 'FontFamily',
|
label: 'Font family',
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
value: 'QlikView Sans, -apple-system, sans-serif',
|
value: 'QlikView Sans, -apple-system, sans-serif',
|
||||||
@@ -133,7 +174,7 @@ const tableFormat = {
|
|||||||
},
|
},
|
||||||
DataFontSize: {
|
DataFontSize: {
|
||||||
ref: 'lettersize',
|
ref: 'lettersize',
|
||||||
translation: 'Font Size',
|
translation: 'Font size',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
component: 'buttongroup',
|
component: 'buttongroup',
|
||||||
options: [
|
options: [
|
||||||
@@ -150,7 +191,7 @@ const tableFormat = {
|
|||||||
},
|
},
|
||||||
textAlignment: {
|
textAlignment: {
|
||||||
ref: 'cellTextAlignment',
|
ref: 'cellTextAlignment',
|
||||||
label: 'Cell Text alignment',
|
label: 'Cell text alignment',
|
||||||
component: 'buttongroup',
|
component: 'buttongroup',
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
@@ -168,15 +209,33 @@ const tableFormat = {
|
|||||||
],
|
],
|
||||||
defaultValue: 'right'
|
defaultValue: 'right'
|
||||||
},
|
},
|
||||||
|
FitChartWidth: {
|
||||||
|
ref: 'fitchartwidth',
|
||||||
|
type: 'boolean',
|
||||||
|
component: 'switch',
|
||||||
|
label: 'Fill chart width',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: true,
|
||||||
|
label: 'On'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: false,
|
||||||
|
label: 'Off'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
ColumnWidthSlider: {
|
ColumnWidthSlider: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
component: 'slider',
|
component: 'slider',
|
||||||
label: 'Column Width',
|
label: 'Column width',
|
||||||
ref: 'columnwidthslider',
|
ref: 'columnwidthslider',
|
||||||
min: 1,
|
min: 20,
|
||||||
max: 3,
|
max: 250,
|
||||||
step: 1,
|
step: 10,
|
||||||
defaultValue: 2
|
defaultValue: 50,
|
||||||
|
show: data => !data.fitchartwidth
|
||||||
},
|
},
|
||||||
SymbolForNulls: {
|
SymbolForNulls: {
|
||||||
ref: 'symbolfornulls',
|
ref: 'symbolfornulls',
|
||||||
@@ -205,7 +264,7 @@ const tableFormat = {
|
|||||||
ref: 'filteroncellclick',
|
ref: 'filteroncellclick',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
component: 'switch',
|
component: 'switch',
|
||||||
label: 'Filter data when cell clicked',
|
label: 'Allow selection in cells',
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
value: true,
|
value: true,
|
||||||
|
|||||||
@@ -1,19 +1,31 @@
|
|||||||
function removeAllTooltips (node) {
|
function cleanupNodes (node) {
|
||||||
const tooltips = node.querySelectorAll('.tooltip');
|
const removables = node.querySelectorAll('.tooltip,input');
|
||||||
[].forEach.call(tooltips, tooltip => {
|
[].forEach.call(removables, removeable => {
|
||||||
if (tooltip.parentNode) {
|
if (removeable.parentNode) {
|
||||||
tooltip.parentNode.removeChild(tooltip);
|
removeable.parentNode.removeChild(removeable);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildTableHTML (title, subtitle, footnote) {
|
function buildTableHTML (containerElement, title, subtitle, footnote) {
|
||||||
const titleHTML = `<p style="font-size:15pt"><b>${title}</b></p>`;
|
const titleHTML = `<p style="font-size:15pt"><b>${title}</b></p>`;
|
||||||
const subtitleHTML = `<p style="font-size:11pt">${subtitle}</p>`;
|
const subtitleHTML = `<p style="font-size:11pt">${subtitle}</p>`;
|
||||||
const footnoteHTML = `<p style="font-size:11pt"><i>Note:</i>${footnote}</p>`;
|
const footnoteHTML = `<p style="font-size:11pt">${footnote}</p>`;
|
||||||
const dataTableClone = document.querySelector('.data-table').cloneNode(true);
|
const kpiTableClone = containerElement[0].querySelector('.kpi-table').cloneNode(true);
|
||||||
|
const dataTableClone = containerElement[0].querySelector('.data-table').cloneNode(true);
|
||||||
|
cleanupNodes(kpiTableClone);
|
||||||
|
cleanupNodes(kpiTableClone);
|
||||||
|
|
||||||
removeAllTooltips(dataTableClone);
|
const kpiTableBodies = kpiTableClone.querySelectorAll('tbody');
|
||||||
|
const dataTableBodies = dataTableClone.querySelectorAll('tbody');
|
||||||
|
const kpiHeader = kpiTableBodies[0].querySelector('tr');
|
||||||
|
const dataTableHeaders = dataTableBodies[0].querySelectorAll('tr');
|
||||||
|
const kpiRows = kpiTableBodies[1].querySelectorAll('tr');
|
||||||
|
const dataRows = dataTableBodies[1].querySelectorAll('tr');
|
||||||
|
let combinedRows = '';
|
||||||
|
for (let i = 0; i < kpiRows.length; i++) {
|
||||||
|
combinedRows += `<tr>${kpiRows[i].innerHTML}${dataRows[i].innerHTML}</tr>`;
|
||||||
|
}
|
||||||
|
|
||||||
const tableHTML = `
|
const tableHTML = `
|
||||||
<html
|
<html
|
||||||
@@ -41,8 +53,23 @@ function buildTableHTML (title, subtitle, footnote) {
|
|||||||
<body>
|
<body>
|
||||||
${titleHTML.length > 0 ? titleHTML : ''}
|
${titleHTML.length > 0 ? titleHTML : ''}
|
||||||
${subtitleHTML.length > 0 ? subtitleHTML : ''}
|
${subtitleHTML.length > 0 ? subtitleHTML : ''}
|
||||||
|
<div>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
${kpiHeader.innerHTML}
|
||||||
|
${dataTableHeaders[0].innerHTML}
|
||||||
|
</tr>
|
||||||
|
${dataTableHeaders.length > 1 ? dataTableHeaders[1].outerHTML : ''}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
${combinedRows}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
${footnoteHTML.length > 0 ? footnoteHTML : ''}
|
${footnoteHTML.length > 0 ? footnoteHTML : ''}
|
||||||
${dataTableClone.outerHTML}
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`.split('>.<')
|
`.split('>.<')
|
||||||
@@ -55,15 +82,15 @@ function buildTableHTML (title, subtitle, footnote) {
|
|||||||
|
|
||||||
function downloadXLS (html) {
|
function downloadXLS (html) {
|
||||||
const filename = 'analysis.xls';
|
const filename = 'analysis.xls';
|
||||||
|
const blobObject = new Blob([html], { type: 'application/vnd.ms-excel' });
|
||||||
|
|
||||||
// IE/Edge
|
// IE/Edge
|
||||||
if (window.navigator.msSaveOrOpenBlob) {
|
if (window.navigator.msSaveOrOpenBlob) {
|
||||||
const blobObject = new Blob([html]);
|
|
||||||
return window.navigator.msSaveOrOpenBlob(blobObject, filename);
|
return window.navigator.msSaveOrOpenBlob(blobObject, filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataURI = generateDataURI(html);
|
|
||||||
const link = window.document.createElement('a');
|
const link = window.document.createElement('a');
|
||||||
link.href = dataURI;
|
link.href = URL.createObjectURL(blobObject);
|
||||||
link.download = filename;
|
link.download = filename;
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
@@ -72,15 +99,8 @@ function downloadXLS (html) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateDataURI (html) {
|
export function exportXLS (containerElement, title, subtitle, footnote) {
|
||||||
const dataType = 'data:application/vnd.ms-excel;base64,';
|
|
||||||
const data = window.btoa(unescape(encodeURIComponent(html)));
|
|
||||||
|
|
||||||
return `${dataType}${data}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function exportXLS (title, subtitle, footnote) {
|
|
||||||
// original was removing icon when starting export, disable and some spinner instead, shouldn't take enough time to warrant either..?
|
// original was removing icon when starting export, disable and some spinner instead, shouldn't take enough time to warrant either..?
|
||||||
const table = buildTableHTML(title, subtitle, footnote);
|
const table = buildTableHTML(containerElement, title, subtitle, footnote);
|
||||||
downloadXLS(table);
|
downloadXLS(table);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { exportXLS } from './excel-export';
|
|
||||||
|
|
||||||
class ExportButton extends React.PureComponent {
|
|
||||||
constructor (props) {
|
|
||||||
super(props);
|
|
||||||
this.handleExport = this.handleExport.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleExport () {
|
|
||||||
const { excelExport, general } = this.props;
|
|
||||||
const { title, subtitle, footnote } = general;
|
|
||||||
if (excelExport) {
|
|
||||||
exportXLS(title, subtitle, footnote);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { excelExport } = this.props;
|
|
||||||
return excelExport === true && (
|
|
||||||
<input
|
|
||||||
className="icon-xls"
|
|
||||||
onClick={this.handleExport}
|
|
||||||
src="/Extensions/qlik-smart-pivot/Excel.png"
|
|
||||||
type="image"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ExportButton.defaultProps = {
|
|
||||||
excelExport: false
|
|
||||||
};
|
|
||||||
|
|
||||||
ExportButton.propTypes = {
|
|
||||||
excelExport: PropTypes.bool,
|
|
||||||
general: PropTypes.shape({}).isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ExportButton;
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { HEADER_FONT_SIZE } from '../initialize-transformed';
|
||||||
import Tooltip from '../tooltip/index.jsx';
|
import Tooltip from '../tooltip/index.jsx';
|
||||||
|
|
||||||
class ColumnHeader extends React.PureComponent {
|
class ColumnHeader extends React.PureComponent {
|
||||||
@@ -9,30 +10,34 @@ class ColumnHeader extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleSelect () {
|
handleSelect () {
|
||||||
const { entry, qlik } = this.props;
|
const { component, entry } = this.props;
|
||||||
qlik.backendApi.selectValues(1, [entry.elementNumber], true);
|
component.backendApi.selectValues(1, [entry.elementNumber], false);
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { baseCSS, cellSuffix, colSpan, entry, styling, qlik } = this.props;
|
const { baseCSS, cellWidth, colSpan, component, entry, styling } = this.props;
|
||||||
const inEditState = qlik.inEditState();
|
const inEditState = component.inEditState();
|
||||||
|
const isMediumFontSize = styling.headerOptions.fontSizeAdjustment === HEADER_FONT_SIZE.MEDIUM;
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
...baseCSS,
|
...baseCSS,
|
||||||
fontSize: `${14 + styling.headerOptions.fontSizeAdjustment}px`,
|
fontSize: `${14 + styling.headerOptions.fontSizeAdjustment}px`,
|
||||||
height: '45px',
|
height: isMediumFontSize ? '43px' : '33px',
|
||||||
verticalAlign: 'middle'
|
verticalAlign: 'middle',
|
||||||
|
minWidth: cellWidth,
|
||||||
|
maxWidth: cellWidth
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<th
|
<th
|
||||||
className={`grid-cells2${cellSuffix}`}
|
className="grid-cells"
|
||||||
colSpan={colSpan}
|
colSpan={colSpan}
|
||||||
onClick={this.handleSelect}
|
onClick={this.handleSelect}
|
||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
isTooltipActive={!inEditState}
|
isTooltipActive={!inEditState}
|
||||||
|
styling={styling}
|
||||||
tooltipText={entry.displayValue}
|
tooltipText={entry.displayValue}
|
||||||
>
|
>
|
||||||
{entry.displayValue}
|
{entry.displayValue}
|
||||||
@@ -43,19 +48,14 @@ class ColumnHeader extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ColumnHeader.defaultProps = {
|
ColumnHeader.defaultProps = {
|
||||||
cellSuffix: '',
|
|
||||||
colSpan: 1
|
colSpan: 1
|
||||||
};
|
};
|
||||||
|
|
||||||
ColumnHeader.propTypes = {
|
ColumnHeader.propTypes = {
|
||||||
baseCSS: PropTypes.shape({}).isRequired,
|
baseCSS: PropTypes.shape({}).isRequired,
|
||||||
cellSuffix: PropTypes.string,
|
cellWidth: PropTypes.string.isRequired,
|
||||||
colSpan: PropTypes.number,
|
colSpan: PropTypes.number,
|
||||||
entry: PropTypes.shape({
|
component: PropTypes.shape({
|
||||||
elementNumber: PropTypes.number.isRequired,
|
|
||||||
name: PropTypes.string.isRequired
|
|
||||||
}).isRequired,
|
|
||||||
qlik: PropTypes.shape({
|
|
||||||
backendApi: PropTypes.shape({
|
backendApi: PropTypes.shape({
|
||||||
selectValues: function (props, propName) {
|
selectValues: function (props, propName) {
|
||||||
if (props.isSnapshot || typeof props[propName] === 'function') {
|
if (props.isSnapshot || typeof props[propName] === 'function') {
|
||||||
@@ -65,6 +65,10 @@ ColumnHeader.propTypes = {
|
|||||||
}
|
}
|
||||||
}).isRequired
|
}).isRequired
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
|
entry: PropTypes.shape({
|
||||||
|
displayValue: PropTypes.string.isRequired,
|
||||||
|
elementNumber: PropTypes.number.isRequired
|
||||||
|
}).isRequired,
|
||||||
styling: PropTypes.shape({
|
styling: PropTypes.shape({
|
||||||
headerOptions: PropTypes.shape({
|
headerOptions: PropTypes.shape({
|
||||||
fontSizeAdjustment: PropTypes.number.isRequired
|
fontSizeAdjustment: PropTypes.number.isRequired
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ExportButton from '../export-button.jsx';
|
|
||||||
import { HEADER_FONT_SIZE } from '../initialize-transformed';
|
import { HEADER_FONT_SIZE } from '../initialize-transformed';
|
||||||
|
import Tooltip from '../tooltip/index.jsx';
|
||||||
|
|
||||||
const ExportColumnHeader = ({ baseCSS, general, title, allowExcelExport, hasSecondDimension, styling }) => {
|
const Dim1Header = ({ component, baseCSS, title, hasSecondDimension, styling }) => {
|
||||||
|
const inEditState = component.inEditState();
|
||||||
const rowSpan = hasSecondDimension ? 2 : 1;
|
const rowSpan = hasSecondDimension ? 2 : 1;
|
||||||
const isMediumFontSize = styling.headerOptions.fontSizeAdjustment === HEADER_FONT_SIZE.MEDIUM;
|
const isMediumFontSize = styling.headerOptions.fontSizeAdjustment === HEADER_FONT_SIZE.MEDIUM;
|
||||||
const style = {
|
const style = {
|
||||||
...baseCSS,
|
...baseCSS,
|
||||||
cursor: 'default',
|
cursor: 'default',
|
||||||
fontSize: `${16 + styling.headerOptions.fontSizeAdjustment}px`,
|
fontSize: `${16 + styling.headerOptions.fontSizeAdjustment}px`,
|
||||||
height: isMediumFontSize ? '100px' : '80px',
|
height: isMediumFontSize ? '90px' : '70px',
|
||||||
verticalAlign: 'middle',
|
verticalAlign: 'middle',
|
||||||
width: '230px'
|
width: '230px'
|
||||||
};
|
};
|
||||||
@@ -21,19 +22,20 @@ const ExportColumnHeader = ({ baseCSS, general, title, allowExcelExport, hasSeco
|
|||||||
rowSpan={rowSpan}
|
rowSpan={rowSpan}
|
||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
<ExportButton
|
<Tooltip
|
||||||
excelExport={allowExcelExport}
|
isTooltipActive={!inEditState}
|
||||||
general={general}
|
styling={styling}
|
||||||
/>
|
tooltipText={title}
|
||||||
{title}
|
>
|
||||||
|
{title}
|
||||||
|
</Tooltip>
|
||||||
</th>
|
</th>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
ExportColumnHeader.propTypes = {
|
Dim1Header.propTypes = {
|
||||||
allowExcelExport: PropTypes.bool.isRequired,
|
|
||||||
baseCSS: PropTypes.shape({}).isRequired,
|
baseCSS: PropTypes.shape({}).isRequired,
|
||||||
general: PropTypes.shape({}).isRequired,
|
component: PropTypes.shape({}).isRequired,
|
||||||
hasSecondDimension: PropTypes.bool.isRequired,
|
hasSecondDimension: PropTypes.bool.isRequired,
|
||||||
styling: PropTypes.shape({
|
styling: PropTypes.shape({
|
||||||
headerOptions: PropTypes.shape({
|
headerOptions: PropTypes.shape({
|
||||||
@@ -43,4 +45,4 @@ ExportColumnHeader.propTypes = {
|
|||||||
title: PropTypes.string.isRequired
|
title: PropTypes.string.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ExportColumnHeader;
|
export default Dim1Header;
|
||||||
@@ -1,121 +1,124 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ExportColumnHeader from './export-column-header.jsx';
|
import Dim1Header from './dim1-header.jsx';
|
||||||
import ColumnHeader from './column-header.jsx';
|
import ColumnHeader from './column-header.jsx';
|
||||||
import MeasurementColumnHeader from './measurement-column-header.jsx';
|
import MeasurementColumnHeader from './measurement-column-header.jsx';
|
||||||
import { injectSeparators } from '../utilities';
|
import { injectSeparators } from '../utilities';
|
||||||
|
|
||||||
const HeadersTable = ({ data, general, qlik, styling }) => {
|
class HeadersTable extends React.PureComponent {
|
||||||
const baseCSS = {
|
render () {
|
||||||
backgroundColor: styling.headerOptions.colorSchema,
|
const {
|
||||||
color: styling.headerOptions.textColor,
|
cellWidth,
|
||||||
fontFamily: styling.options.fontFamily,
|
columnSeparatorWidth,
|
||||||
textAlign: styling.headerOptions.alignment
|
component,
|
||||||
};
|
data,
|
||||||
|
isKpi,
|
||||||
|
styling
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const baseCSS = {
|
||||||
dimension1,
|
backgroundColor: styling.headerOptions.colorSchema,
|
||||||
dimension2,
|
color: styling.headerOptions.textColor,
|
||||||
measurements
|
fontFamily: styling.options.fontFamily,
|
||||||
} = data.headers;
|
textAlign: styling.headerOptions.alignment
|
||||||
|
};
|
||||||
|
|
||||||
const hasSecondDimension = dimension2.length > 0;
|
const {
|
||||||
|
dimension1,
|
||||||
|
dimension2,
|
||||||
|
measurements
|
||||||
|
} = data.headers;
|
||||||
|
|
||||||
return (
|
const hasSecondDimension = dimension2.length > 0;
|
||||||
<div className="header-wrapper">
|
|
||||||
<table className="header">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<ExportColumnHeader
|
|
||||||
allowExcelExport={general.allowExcelExport}
|
|
||||||
baseCSS={baseCSS}
|
|
||||||
general={general}
|
|
||||||
hasSecondDimension={hasSecondDimension}
|
|
||||||
styling={styling}
|
|
||||||
title={dimension1[0].name}
|
|
||||||
/>
|
|
||||||
{!hasSecondDimension && measurements.map(measurementEntry => (
|
|
||||||
<MeasurementColumnHeader
|
|
||||||
baseCSS={baseCSS}
|
|
||||||
general={general}
|
|
||||||
hasSecondDimension={hasSecondDimension}
|
|
||||||
key={`${measurementEntry.displayValue}-${measurementEntry.name}`}
|
|
||||||
measurement={measurementEntry}
|
|
||||||
styling={styling}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{hasSecondDimension && injectSeparators(dimension2, styling.useSeparatorColumns).map((entry, index) => {
|
|
||||||
if (entry.isSeparator) {
|
|
||||||
const separatorStyle = {
|
|
||||||
color: 'white',
|
|
||||||
fontFamily: styling.options.fontFamily,
|
|
||||||
fontSize: `${13 + styling.headerOptions.fontSizeAdjustment}px`
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
const separatorStyle = {
|
||||||
<th
|
minWidth: columnSeparatorWidth,
|
||||||
className="empty"
|
maxWidth: columnSeparatorWidth
|
||||||
key={index}
|
};
|
||||||
style={separatorStyle}
|
|
||||||
>
|
return (
|
||||||
*
|
<div className="header-wrapper">
|
||||||
</th>
|
<table className="header">
|
||||||
);
|
<tbody>
|
||||||
}
|
<tr>
|
||||||
return (
|
{isKpi ?
|
||||||
<ColumnHeader
|
<Dim1Header
|
||||||
baseCSS={baseCSS}
|
baseCSS={baseCSS}
|
||||||
cellSuffix={general.cellSuffix}
|
component={component}
|
||||||
colSpan={measurements.length}
|
hasSecondDimension={hasSecondDimension}
|
||||||
entry={entry}
|
styling={styling}
|
||||||
key={entry.displayValue}
|
title={dimension1[0].name}
|
||||||
qlik={qlik}
|
/> : null
|
||||||
|
}
|
||||||
|
{!isKpi && !hasSecondDimension && measurements.map(measurementEntry => (
|
||||||
|
<MeasurementColumnHeader
|
||||||
|
baseCSS={baseCSS}
|
||||||
|
cellWidth={cellWidth}
|
||||||
|
hasSecondDimension={hasSecondDimension}
|
||||||
|
key={`${measurementEntry.displayValue}-${measurementEntry.name}-${measurementEntry.index}`}
|
||||||
|
measurement={measurementEntry}
|
||||||
styling={styling}
|
styling={styling}
|
||||||
/>
|
/>
|
||||||
);
|
))}
|
||||||
})}
|
{!isKpi && hasSecondDimension && injectSeparators(dimension2, columnSeparatorWidth).map((entry, index) => {
|
||||||
</tr>
|
if (entry.isSeparator) {
|
||||||
{hasSecondDimension && (
|
|
||||||
<tr>
|
|
||||||
{injectSeparators(dimension2, styling.useSeparatorColumns).map((dimensionEntry, index) => {
|
|
||||||
if (dimensionEntry.isSeparator) {
|
|
||||||
const separatorStyle = {
|
|
||||||
color: 'white',
|
|
||||||
fontFamily: styling.options.fontFamily,
|
|
||||||
fontSize: `${12 + styling.headerOptions.fontSizeAdjustment}px`
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<th
|
<th
|
||||||
className="empty"
|
className="empty"
|
||||||
key={index}
|
key={index}
|
||||||
style={separatorStyle}
|
style={separatorStyle}
|
||||||
>
|
/>
|
||||||
*
|
|
||||||
</th>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return measurements.map(measurementEntry => (
|
return (
|
||||||
<MeasurementColumnHeader
|
<ColumnHeader
|
||||||
baseCSS={baseCSS}
|
baseCSS={baseCSS}
|
||||||
dimensionEntry={dimensionEntry}
|
cellWidth={cellWidth}
|
||||||
general={general}
|
colSpan={measurements.length}
|
||||||
hasSecondDimension={hasSecondDimension}
|
component={component}
|
||||||
key={`${measurementEntry.displayValue}-${measurementEntry.name}-${dimensionEntry.name}`}
|
entry={entry}
|
||||||
measurement={measurementEntry}
|
key={entry.displayValue}
|
||||||
styling={styling}
|
styling={styling}
|
||||||
/>
|
/>
|
||||||
));
|
);
|
||||||
})}
|
})}
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
{!isKpi && hasSecondDimension && (
|
||||||
</tbody>
|
<tr>
|
||||||
</table>
|
{injectSeparators(dimension2, columnSeparatorWidth).map((dimensionEntry, index) => {
|
||||||
</div>
|
if (dimensionEntry.isSeparator) {
|
||||||
);
|
return (
|
||||||
};
|
<th
|
||||||
|
className="empty"
|
||||||
|
key={index}
|
||||||
|
style={separatorStyle}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return measurements.map(measurementEntry => (
|
||||||
|
<MeasurementColumnHeader
|
||||||
|
baseCSS={baseCSS}
|
||||||
|
cellWidth={cellWidth}
|
||||||
|
dimensionEntry={dimensionEntry}
|
||||||
|
hasSecondDimension={hasSecondDimension}
|
||||||
|
key={`${measurementEntry.displayValue}-${measurementEntry.name}-${measurementEntry.index}-${dimensionEntry.name}`}
|
||||||
|
measurement={measurementEntry}
|
||||||
|
styling={styling}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
HeadersTable.propTypes = {
|
HeadersTable.propTypes = {
|
||||||
|
cellWidth: PropTypes.string.isRequired,
|
||||||
|
columnSeparatorWidth: PropTypes.string.isRequired,
|
||||||
data: PropTypes.shape({
|
data: PropTypes.shape({
|
||||||
headers: PropTypes.shape({
|
headers: PropTypes.shape({
|
||||||
dimension1: PropTypes.array,
|
dimension1: PropTypes.array,
|
||||||
@@ -123,21 +126,12 @@ HeadersTable.propTypes = {
|
|||||||
measurements: PropTypes.array
|
measurements: PropTypes.array
|
||||||
})
|
})
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
general: PropTypes.shape({}).isRequired,
|
component: PropTypes.shape({}).isRequired,
|
||||||
qlik: PropTypes.shape({
|
|
||||||
backendApi: PropTypes.shape({
|
|
||||||
selectValues: function (props, propName) {
|
|
||||||
if (props.isSnapshot || typeof props[propName] === 'function') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return new Error('Missing implementation of qlik.backendApi.selectValues.');
|
|
||||||
}
|
|
||||||
}).isRequired
|
|
||||||
}).isRequired,
|
|
||||||
styling: PropTypes.shape({
|
styling: PropTypes.shape({
|
||||||
headerOptions: PropTypes.shape({}),
|
headerOptions: PropTypes.shape({}),
|
||||||
options: PropTypes.shape({})
|
options: PropTypes.shape({})
|
||||||
}).isRequired
|
}).isRequired,
|
||||||
|
isKpi: PropTypes.bool.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default HeadersTable;
|
export default HeadersTable;
|
||||||
|
|||||||
@@ -3,62 +3,55 @@ import PropTypes from 'prop-types';
|
|||||||
import { HEADER_FONT_SIZE } from '../initialize-transformed';
|
import { HEADER_FONT_SIZE } from '../initialize-transformed';
|
||||||
import Tooltip from '../tooltip/index.jsx';
|
import Tooltip from '../tooltip/index.jsx';
|
||||||
|
|
||||||
const MeasurementColumnHeader = ({ baseCSS, general, hasSecondDimension, measurement, styling }) => {
|
const MeasurementColumnHeader = ({ baseCSS, cellWidth, hasSecondDimension, measurement, styling }) => {
|
||||||
const title = `${measurement.name} ${measurement.magnitudeLabelSuffix}`;
|
const title = `${measurement.name}`;
|
||||||
const { fontSizeAdjustment } = styling.headerOptions;
|
const { fontSizeAdjustment } = styling.headerOptions;
|
||||||
const isMediumFontSize = styling.headerOptions.fontSizeAdjustment === HEADER_FONT_SIZE.MEDIUM;
|
const isMediumFontSize = fontSizeAdjustment === HEADER_FONT_SIZE.MEDIUM;
|
||||||
|
|
||||||
|
const cellStyle = {
|
||||||
|
...baseCSS,
|
||||||
|
verticalAlign: 'middle',
|
||||||
|
minWidth: cellWidth,
|
||||||
|
maxWidth: cellWidth
|
||||||
|
};
|
||||||
|
|
||||||
if (hasSecondDimension) {
|
if (hasSecondDimension) {
|
||||||
const isPercentageFormat = measurement.format.substring(measurement.format.length - 1) === '%';
|
const isPercentageFormat = measurement.format.substring(measurement.format.length - 1) === '%';
|
||||||
let baseFontSize = 14;
|
let baseFontSize = 14;
|
||||||
let cellClass = 'grid-cells2';
|
|
||||||
if (isPercentageFormat) {
|
if (isPercentageFormat) {
|
||||||
baseFontSize = 13;
|
baseFontSize = 13;
|
||||||
cellClass = 'grid-cells2-small';
|
|
||||||
}
|
}
|
||||||
const cellStyle = {
|
cellStyle.fontSize = `${baseFontSize + fontSizeAdjustment}px`;
|
||||||
...baseCSS,
|
cellStyle.height = isMediumFontSize ? '45px' : '35px';
|
||||||
cursor: 'default',
|
|
||||||
fontSize: `${baseFontSize + fontSizeAdjustment}px`,
|
|
||||||
height: isMediumFontSize ? '50px' : '25px',
|
|
||||||
verticalAlign: 'middle'
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<th
|
<th
|
||||||
className={`${cellClass}${general.cellSuffix}`}
|
className="grid-cells"
|
||||||
style={cellStyle}
|
style={cellStyle}
|
||||||
>
|
>
|
||||||
<span className="wrapclass25">
|
<Tooltip
|
||||||
<Tooltip
|
tooltipText={title}
|
||||||
tooltipText={title}
|
styling={styling}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</span>
|
|
||||||
</th>
|
</th>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLong = (title.length > 11 && fontSizeAdjustment === 0) || (title.length > 12 && fontSizeAdjustment === -2);
|
cellStyle.fontSize = `${15 + fontSizeAdjustment}px`;
|
||||||
const suffixWrap = isLong ? '70' : 'empty';
|
cellStyle.height = isMediumFontSize ? '90px' : '70px';
|
||||||
const style = {
|
|
||||||
...baseCSS,
|
|
||||||
cursor: 'default',
|
|
||||||
fontSize: `${15 + fontSizeAdjustment}px`,
|
|
||||||
height: isMediumFontSize ? '100px' : '80px',
|
|
||||||
verticalAlign: 'middle'
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<th
|
<th
|
||||||
className={`grid-cells2 ${general.cellSuffix}`}
|
className="grid-cells"
|
||||||
style={style}
|
style={cellStyle}
|
||||||
>
|
>
|
||||||
<span
|
<Tooltip
|
||||||
className={`wrapclass${suffixWrap}`}
|
tooltipText={title}
|
||||||
style={{ fontFamily: styling.headerOptions.fontFamily }}
|
styling={styling}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</span>
|
</Tooltip>
|
||||||
</th>
|
</th>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -69,9 +62,7 @@ MeasurementColumnHeader.defaultProps = {
|
|||||||
|
|
||||||
MeasurementColumnHeader.propTypes = {
|
MeasurementColumnHeader.propTypes = {
|
||||||
baseCSS: PropTypes.shape({}).isRequired,
|
baseCSS: PropTypes.shape({}).isRequired,
|
||||||
general: PropTypes.shape({
|
cellWidth: PropTypes.string.isRequired,
|
||||||
cellSuffix: PropTypes.string.isRequired
|
|
||||||
}).isRequired,
|
|
||||||
hasSecondDimension: PropTypes.bool,
|
hasSecondDimension: PropTypes.bool,
|
||||||
measurement: PropTypes.shape({
|
measurement: PropTypes.shape({
|
||||||
name: PropTypes.string.isRequired
|
name: PropTypes.string.isRequired
|
||||||
|
|||||||
112
src/index.js
112
src/index.js
@@ -1,17 +1,19 @@
|
|||||||
import paint from './paint.jsx';
|
import definition from "./definition";
|
||||||
import definition from './definition';
|
import { exportXLS } from "./excel-export";
|
||||||
import './main.less';
|
import { initializeDataCube, initializeDesignList } from "./dataset";
|
||||||
|
import initializeStore from "./store";
|
||||||
|
import qlik from "qlik";
|
||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
import Root from "./root.jsx";
|
||||||
|
import "./main.less";
|
||||||
|
|
||||||
if (!window._babelPolyfill) { // eslint-disable-line no-underscore-dangle
|
if (!window._babelPolyfill) {
|
||||||
require('@babel/polyfill'); // eslint-disable-line global-require
|
// eslint-disable-line no-underscore-dangle
|
||||||
|
require("@babel/polyfill"); // eslint-disable-line global-require
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
controller: [
|
|
||||||
'$scope',
|
|
||||||
'$timeout',
|
|
||||||
function controller () {}
|
|
||||||
],
|
|
||||||
design: {
|
design: {
|
||||||
dimensions: {
|
dimensions: {
|
||||||
max: 1,
|
max: 1,
|
||||||
@@ -20,16 +22,23 @@ export default {
|
|||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
dimensions: {
|
dimensions: {
|
||||||
max: 3,
|
max (nMeasures) {
|
||||||
|
return nMeasures < 9 ? 2 : 1;
|
||||||
|
},
|
||||||
min: 1,
|
min: 1,
|
||||||
uses: 'dimensions'
|
uses: 'dimensions'
|
||||||
},
|
},
|
||||||
measures: {
|
measures: {
|
||||||
max: 8,
|
max (nDims) {
|
||||||
|
return nDims < 2 ? 9 : 8;
|
||||||
|
},
|
||||||
min: 1,
|
min: 1,
|
||||||
uses: 'measures'
|
uses: 'measures'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// Prevent conversion from and to this object
|
||||||
|
exportProperties: null,
|
||||||
|
importProperties: null,
|
||||||
definition,
|
definition,
|
||||||
initialProperties: {
|
initialProperties: {
|
||||||
version: 1.0,
|
version: 1.0,
|
||||||
@@ -37,11 +46,14 @@ export default {
|
|||||||
qDimensions: [],
|
qDimensions: [],
|
||||||
qInitialDataFetch: [
|
qInitialDataFetch: [
|
||||||
{
|
{
|
||||||
qHeight: 1,
|
qTop: 0,
|
||||||
qWidth: 10
|
qLeft: 0,
|
||||||
|
qWidth: 50,
|
||||||
|
qHeight: 50
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
qMeasures: []
|
qMeasures: [],
|
||||||
|
qSuppressZero: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
support: {
|
support: {
|
||||||
@@ -49,16 +61,74 @@ export default {
|
|||||||
exportData: true,
|
exportData: true,
|
||||||
snapshot: true
|
snapshot: true
|
||||||
},
|
},
|
||||||
paint ($element, layout) {
|
async paint ($element, layout, requestPage) {
|
||||||
try {
|
const dataCube = await initializeDataCube(this, layout, requestPage);
|
||||||
paint($element, layout, this);
|
const editmodeClass = this.inAnalysisState() ? '' : 'edit-mode';
|
||||||
} catch (exception) {
|
let state, designList;
|
||||||
console.error(exception); // eslint-disable-line no-console
|
if (dataCube === null) {
|
||||||
throw exception;
|
state = {
|
||||||
|
$element,
|
||||||
|
component: this,
|
||||||
|
dataCube,
|
||||||
|
designList,
|
||||||
|
layout,
|
||||||
|
error: true
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
designList = await initializeDesignList(this, layout);
|
||||||
|
state = await initializeStore({
|
||||||
|
$element,
|
||||||
|
component: this,
|
||||||
|
dataCube,
|
||||||
|
designList,
|
||||||
|
layout,
|
||||||
|
error: false
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
const jsx = (
|
||||||
|
<Root editmodeClass={editmodeClass} component={this} state={state} />
|
||||||
|
);
|
||||||
|
|
||||||
|
ReactDOM.render(jsx, $element[0]);
|
||||||
},
|
},
|
||||||
snapshot: {
|
snapshot: {
|
||||||
canTakeSnapshot: true
|
canTakeSnapshot: true
|
||||||
},
|
},
|
||||||
|
async setSnapshotData (snapshotLayout) {
|
||||||
|
snapshotLayout.snapshotData.dataCube = await initializeDataCube(
|
||||||
|
this,
|
||||||
|
snapshotLayout
|
||||||
|
);
|
||||||
|
snapshotLayout.snapshotData.designList = await initializeDesignList(
|
||||||
|
this,
|
||||||
|
snapshotLayout
|
||||||
|
);
|
||||||
|
return snapshotLayout;
|
||||||
|
},
|
||||||
|
async getContextMenu (obj, menu) {
|
||||||
|
const app = qlik.currApp(this);
|
||||||
|
const isPersonalResult = await app.global.isPersonalMode();
|
||||||
|
if (
|
||||||
|
!this.$scope.layout.allowexportxls ||
|
||||||
|
(isPersonalResult && isPersonalResult.qReturn)
|
||||||
|
) {
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.addItem({
|
||||||
|
translation: 'Export as XLS',
|
||||||
|
tid: 'export-excel',
|
||||||
|
icon: 'export',
|
||||||
|
select: () => {
|
||||||
|
exportXLS(
|
||||||
|
this.$element,
|
||||||
|
this.$scope.layout.title,
|
||||||
|
this.$scope.layout.subtitle,
|
||||||
|
this.$scope.layout.footnote
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return menu;
|
||||||
|
},
|
||||||
version: 1.0
|
version: 1.0
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,47 +18,16 @@ function getAlignment (option) {
|
|||||||
function getFontSizeAdjustment (option) {
|
function getFontSizeAdjustment (option) {
|
||||||
const fontSizeAdjustmentOptions = {
|
const fontSizeAdjustmentOptions = {
|
||||||
1: HEADER_FONT_SIZE.SMALL,
|
1: HEADER_FONT_SIZE.SMALL,
|
||||||
2: HEADER_FONT_SIZE.MEDIUM,
|
2: HEADER_FONT_SIZE.MEDIUM
|
||||||
3: 2
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return fontSizeAdjustmentOptions[option] || 0;
|
return fontSizeAdjustmentOptions[option] || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCellSuffix (option) {
|
|
||||||
const cellSuffixOptions = {
|
|
||||||
1: '-s',
|
|
||||||
3: '-l'
|
|
||||||
};
|
|
||||||
|
|
||||||
return cellSuffixOptions[option] || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMeasurementFormat (measurement) {
|
|
||||||
if (measurement.qNumFormat.qType === 'U' || measurement.qNumFormat.qFmt === '##############') {
|
|
||||||
return '#.##0';
|
|
||||||
} else if (measurement.qNumFormat.qType === 'R') {
|
|
||||||
return measurement.qNumFormat.qFmt.replace(/(|)/gi, '');
|
|
||||||
}
|
|
||||||
return measurement.qNumFormat.qFmt;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMagnitudeLabelSuffix (magnitudeOption) {
|
|
||||||
const magnitudeLabelSuffixOptions = {
|
|
||||||
'k': ' (k)',
|
|
||||||
'm': ' (m)'
|
|
||||||
};
|
|
||||||
|
|
||||||
return magnitudeLabelSuffixOptions[magnitudeOption] || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateMeasurements (information) {
|
function generateMeasurements (information) {
|
||||||
return information.map(measurement => {
|
return information.map(measurement => {
|
||||||
const format = getMeasurementFormat(measurement);
|
|
||||||
const formatMagnitude = format.substr(format.length - 1).toLowerCase();
|
|
||||||
const transformedMeasurement = {
|
const transformedMeasurement = {
|
||||||
format,
|
format: measurement.qNumFormat.qFmt || '#.##0',
|
||||||
magnitudeLabelSuffix: getMagnitudeLabelSuffix(formatMagnitude),
|
|
||||||
name: measurement.qFallbackTitle
|
name: measurement.qFallbackTitle
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -68,7 +37,7 @@ function generateMeasurements (information) {
|
|||||||
|
|
||||||
function generateDimensionEntry (information, data) {
|
function generateDimensionEntry (information, data) {
|
||||||
return {
|
return {
|
||||||
displayValue: data.qText,
|
displayValue: data.qText || data.qNum,
|
||||||
elementNumber: data.qElemNumber,
|
elementNumber: data.qElemNumber,
|
||||||
name: information.qFallbackTitle,
|
name: information.qFallbackTitle,
|
||||||
value: data.qNum
|
value: data.qNum
|
||||||
@@ -79,11 +48,6 @@ function generateMatrixCell ({ cell, dimension1Information, dimension2Informatio
|
|||||||
const matrixCell = {
|
const matrixCell = {
|
||||||
displayValue: cell.qText,
|
displayValue: cell.qText,
|
||||||
format: measurementInformation.format,
|
format: measurementInformation.format,
|
||||||
magnitude: measurementInformation.magnitudeLabelSuffix.substring(
|
|
||||||
measurementInformation.magnitudeLabelSuffix.length - 2,
|
|
||||||
measurementInformation.magnitudeLabelSuffix.length - 1
|
|
||||||
),
|
|
||||||
magnitudeLabelSuffix: measurementInformation.magnitudeLabelSuffix,
|
|
||||||
name: measurementInformation.name,
|
name: measurementInformation.name,
|
||||||
parents: {
|
parents: {
|
||||||
dimension1: {
|
dimension1: {
|
||||||
@@ -91,7 +55,8 @@ function generateMatrixCell ({ cell, dimension1Information, dimension2Informatio
|
|||||||
header: dimension1Information.qText
|
header: dimension1Information.qText
|
||||||
},
|
},
|
||||||
measurement: {
|
measurement: {
|
||||||
header: measurementInformation.name
|
header: measurementInformation.name,
|
||||||
|
index: measurementInformation.index
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
value: cell.qNum
|
value: cell.qNum
|
||||||
@@ -99,103 +64,161 @@ function generateMatrixCell ({ cell, dimension1Information, dimension2Informatio
|
|||||||
|
|
||||||
if (dimension2Information) {
|
if (dimension2Information) {
|
||||||
matrixCell.parents.dimension2 = {
|
matrixCell.parents.dimension2 = {
|
||||||
elementNumber: dimension2Information.qElemNumber
|
elementNumber: dimension2Information.qElemNumber,
|
||||||
|
header: dimension2Information.qText
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return matrixCell;
|
return matrixCell;
|
||||||
}
|
}
|
||||||
|
|
||||||
let lastRow = 0;
|
|
||||||
function generateDataSet (component, dimensionsInformation, measurementsInformation, cubes) {
|
function generateDataSet (component, dimensionsInformation, measurementsInformation, dataCube) {
|
||||||
const dimension1 = [];
|
|
||||||
const dimension2 = [];
|
|
||||||
const measurements = generateMeasurements(measurementsInformation);
|
const measurements = generateMeasurements(measurementsInformation);
|
||||||
|
let dimension1 = [];
|
||||||
|
let dimension2 = [];
|
||||||
let matrix = [];
|
let matrix = [];
|
||||||
|
|
||||||
let previousDim1Entry;
|
const hasSecondDimension = dimensionsInformation.length > 1;
|
||||||
const hasDesignDimension = cubes.design;
|
// eslint-disable-next-line no-undefined
|
||||||
const hasSecondDimension = hasDesignDimension ? dimensionsInformation.length > 2 : dimensionsInformation.length > 1;
|
for (let index = 0; dataCube[index] !== undefined; index++) {
|
||||||
cubes.data.forEach(row => {
|
// eslint-disable-next-line no-loop-func
|
||||||
lastRow += 1;
|
dataCube[index].forEach(row => {
|
||||||
const dimension1Entry = generateDimensionEntry(dimensionsInformation[0], row[0]);
|
const dimension1Entry = generateDimensionEntry(dimensionsInformation[0], row[0]);
|
||||||
dimension1.push(dimension1Entry);
|
dimension1.push(dimension1Entry);
|
||||||
let dimension2Entry;
|
let dimension2Entry;
|
||||||
let firstDataCell = 1;
|
let firstDataCell = 1;
|
||||||
if (hasSecondDimension) {
|
if (hasSecondDimension) {
|
||||||
dimension2Entry = generateDimensionEntry(dimensionsInformation[1], row[1]);
|
dimension2Entry = generateDimensionEntry(dimensionsInformation[1], row[1]);
|
||||||
dimension2.push(dimension2Entry);
|
dimension2.push(dimension2Entry);
|
||||||
firstDataCell = 2;
|
firstDataCell = 2;
|
||||||
}
|
}
|
||||||
const matrixRow = row
|
let matrixRow = row
|
||||||
.slice(firstDataCell, row.length)
|
.slice(firstDataCell, row.length)
|
||||||
.map((cell, cellIndex) => {
|
.map((cell, cellIndex) => {
|
||||||
const measurementInformation = measurements[cellIndex];
|
const measurementInformation = measurements[cellIndex];
|
||||||
const dimension1Information = row[0]; // eslint-disable-line prefer-destructuring
|
measurementInformation.index = cellIndex;
|
||||||
const dimension2Information = hasSecondDimension ? row[1] : null;
|
const dimension1Information = row[0]; // eslint-disable-line prefer-destructuring
|
||||||
const generatedCell = generateMatrixCell({
|
const dimension2Information = hasSecondDimension ? row[1] : null;
|
||||||
cell,
|
const generatedCell = generateMatrixCell({
|
||||||
dimension1Information,
|
cell,
|
||||||
dimension2Information,
|
dimension1Information,
|
||||||
measurementInformation
|
dimension2Information,
|
||||||
|
measurementInformation
|
||||||
|
});
|
||||||
|
return generatedCell;
|
||||||
});
|
});
|
||||||
|
|
||||||
return generatedCell;
|
let appendToRowIndex = matrix.length;
|
||||||
});
|
if (hasSecondDimension) {
|
||||||
|
// See if there already is a row for the current dim1
|
||||||
if (hasSecondDimension) {
|
for (let i = 0; i < matrix.length; i++) {
|
||||||
const currentDim1Entry = row[0].qText;
|
if (matrix[i][0].parents.dimension1.header === matrixRow[0].parents.dimension1.header) {
|
||||||
const isSameDimension1AsPrevious = currentDim1Entry === previousDim1Entry;
|
appendToRowIndex = i;
|
||||||
if (isSameDimension1AsPrevious) {
|
matrixRow = matrix[i].concat(matrixRow);
|
||||||
const updatedRow = matrix[matrix.length - 1].concat(matrixRow);
|
}
|
||||||
|
}
|
||||||
matrix = [
|
|
||||||
...matrix.slice(0, matrix.length - 1),
|
|
||||||
updatedRow
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
matrix[matrix.length] = matrixRow;
|
|
||||||
}
|
}
|
||||||
previousDim1Entry = currentDim1Entry;
|
matrix[appendToRowIndex] = matrixRow;
|
||||||
} else {
|
});
|
||||||
matrix[matrix.length] = matrixRow;
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// filter header dimensions to only have distinct values
|
// filter header dimensions to only have distinct values
|
||||||
|
dimension1 = distinctArray(dimension1);
|
||||||
|
dimension2 = distinctArray(dimension2);
|
||||||
|
|
||||||
|
// Make sure all rows are saturated, otherwise data risks being displayed in the wrong column
|
||||||
|
matrix = matrix.map((row, rowIndex) => {
|
||||||
|
if ((hasSecondDimension && row.length == (dimension2.length * measurements.length))
|
||||||
|
|| (!hasSecondDimension && row.length == measurements.length)) {
|
||||||
|
// Row is saturated
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row is not saturated, so must add empty cells to fill the gaps
|
||||||
|
let newRow = [];
|
||||||
|
if (hasSecondDimension) {
|
||||||
|
// Got a second dimension, so need to add measurements for all values of the second dimension
|
||||||
|
let rowDataIndex = 0;
|
||||||
|
dimension2.forEach(dim => {
|
||||||
|
rowDataIndex = appendMissingCells(
|
||||||
|
row, newRow, rowDataIndex, measurements, rowIndex, dim, dimension1);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
appendMissingCells(row, newRow, 0, measurements, rowIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newRow;
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dimension1: distinctArray(dimension1),
|
dimension1: dimension1,
|
||||||
dimension2: distinctArray(dimension2),
|
dimension2: dimension2,
|
||||||
matrix,
|
matrix,
|
||||||
measurements
|
measurements
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeTransformed ({ $element, component, cubes, layout }) {
|
/*
|
||||||
|
* Appends the cells of the source row, as well as those missing, to the destination row, starting
|
||||||
|
* from the given source index. Returns the source index of the next source cell after this has
|
||||||
|
* completed. If there is a second dimension the dim2ElementNumber should be set to the current
|
||||||
|
* index of the dimension2 value being processed.
|
||||||
|
*/
|
||||||
|
function appendMissingCells (
|
||||||
|
sourceRow, destRow, sourceIndex, measurements, matrixIndex, dim2, dim1) {
|
||||||
|
|
||||||
|
let index = sourceIndex;
|
||||||
|
measurements.forEach((measurement, measureIndex) => {
|
||||||
|
if (index < sourceRow.length) {
|
||||||
|
// Source contains the expected cell
|
||||||
|
destRow.push(sourceRow[index]);
|
||||||
|
index++;
|
||||||
|
} else {
|
||||||
|
// Source doesn't contain the expected cell, so add empty
|
||||||
|
destRow.push({
|
||||||
|
displayValue: '',
|
||||||
|
parents: {
|
||||||
|
dimension1: {
|
||||||
|
elementNumber: dim1[matrixIndex].elementNumber,
|
||||||
|
header: dim1[matrixIndex].displayValue
|
||||||
|
},
|
||||||
|
dimension2: {
|
||||||
|
elementNumber: dim2.elementNumber,
|
||||||
|
header: dim2.displayValue
|
||||||
|
},
|
||||||
|
measurement: {
|
||||||
|
header: measurement.name,
|
||||||
|
index: measureIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeTransformed ({ component, dataCube, designList, layout }) {
|
||||||
const dimensionsInformation = component.backendApi.getDimensionInfos();
|
const dimensionsInformation = component.backendApi.getDimensionInfos();
|
||||||
const measurementsInformation = component.backendApi.getMeasureInfos();
|
const measurementsInformation = component.backendApi.getMeasureInfos();
|
||||||
const dimensionCount = layout.qHyperCube.qDimensionInfo.length;
|
const dimensionCount = layout.qHyperCube.qDimensionInfo.length;
|
||||||
const rowCount = component.backendApi.getRowCount();
|
|
||||||
const maxLoops = layout.maxloops;
|
|
||||||
const {
|
const {
|
||||||
dimension1,
|
dimension1,
|
||||||
dimension2,
|
dimension2,
|
||||||
measurements,
|
measurements,
|
||||||
matrix
|
matrix
|
||||||
} = generateDataSet(component, dimensionsInformation, measurementsInformation, cubes);
|
} = generateDataSet(component, dimensionsInformation, measurementsInformation, dataCube);
|
||||||
|
|
||||||
const customSchemaBasic = [];
|
const customSchemaBasic = [];
|
||||||
const customSchemaFull = [];
|
const customSchemaFull = [];
|
||||||
let customHeadersCount = 0;
|
let customHeadersCount = 0;
|
||||||
|
|
||||||
if (cubes.design) {
|
if (designList && designList.length > 0) {
|
||||||
const allTextLines = cubes.design.map(entry => entry[0].qText);
|
const headers = designList[0].split(';');
|
||||||
const headers = allTextLines[0].split(';');
|
|
||||||
customHeadersCount = headers.length;
|
customHeadersCount = headers.length;
|
||||||
for (let lineNumber = 0; lineNumber < allTextLines.length; lineNumber += 1) {
|
for (let lineNumber = 0; lineNumber < designList.length; lineNumber += 1) {
|
||||||
customSchemaFull[lineNumber] = new Array(headers.length);
|
customSchemaFull[lineNumber] = new Array(headers.length);
|
||||||
const data = allTextLines[lineNumber].split(';');
|
const data = designList[lineNumber].split(';');
|
||||||
|
|
||||||
if (data.length === headers.length) {
|
if (data.length === headers.length) {
|
||||||
for (let headerIndex = 0; headerIndex < headers.length; headerIndex += 1) {
|
for (let headerIndex = 0; headerIndex < headers.length; headerIndex += 1) {
|
||||||
@@ -206,6 +229,18 @@ function initializeTransformed ({ $element, component, cubes, layout }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let cellWidth;
|
||||||
|
if (layout.fitchartwidth) {
|
||||||
|
// The widths are calculated based on the current element width. Note: this could use % to set
|
||||||
|
// the widths as percentages of the available width. However, this often results in random
|
||||||
|
// columns getting 1px wider than the others because of rounding necessary to fill the width.
|
||||||
|
// This 1px causes missalignment between the data- and header tables.
|
||||||
|
cellWidth = '';
|
||||||
|
} else {
|
||||||
|
// If using the previous solution just set 60px
|
||||||
|
cellWidth = `${layout.columnwidthslider > 10 ? layout.columnwidthslider : 60}px`;
|
||||||
|
}
|
||||||
|
|
||||||
// top level properties could be reducers and then components connect to grab what they want,
|
// top level properties could be reducers and then components connect to grab what they want,
|
||||||
// possibly with reselect for some presentational transforms (moving some of the presentational logic like formatting and such)
|
// possibly with reselect for some presentational transforms (moving some of the presentational logic like formatting and such)
|
||||||
const transformedProperties = {
|
const transformedProperties = {
|
||||||
@@ -223,12 +258,12 @@ function initializeTransformed ({ $element, component, cubes, layout }) {
|
|||||||
general: {
|
general: {
|
||||||
allowExcelExport: layout.allowexportxls,
|
allowExcelExport: layout.allowexportxls,
|
||||||
allowFilteringByClick: layout.filteroncellclick,
|
allowFilteringByClick: layout.filteroncellclick,
|
||||||
cellSuffix: getCellSuffix(layout.columnwidthslider), // TOOD: move to matrix cells or is it headers.measurements?
|
cellWidth: cellWidth,
|
||||||
errorMessage: layout.errormessage,
|
errorMessage: layout.errormessage,
|
||||||
footnote: layout.footnote,
|
footnote: layout.footnote,
|
||||||
maxLoops,
|
|
||||||
subtitle: layout.subtitle,
|
subtitle: layout.subtitle,
|
||||||
title: layout.title
|
title: layout.title,
|
||||||
|
useColumnSeparator: layout.separatorcols && dimensionCount > 1
|
||||||
},
|
},
|
||||||
selection: {
|
selection: {
|
||||||
dimensionSelectionCounts: dimensionsInformation.map(dimensionInfo => dimensionInfo.qStateCounts.qSelected)
|
dimensionSelectionCounts: dimensionsInformation.map(dimensionInfo => dimensionInfo.qStateCounts.qSelected)
|
||||||
@@ -239,7 +274,7 @@ function initializeTransformed ({ $element, component, cubes, layout }) {
|
|||||||
count: customHeadersCount,
|
count: customHeadersCount,
|
||||||
full: customSchemaFull
|
full: customSchemaFull
|
||||||
},
|
},
|
||||||
hasCustomFileStyle: Boolean(cubes.design),
|
hasCustomFileStyle: Boolean(designList),
|
||||||
headerOptions: {
|
headerOptions: {
|
||||||
alignment: getAlignment(layout.HeaderAlign),
|
alignment: getAlignment(layout.HeaderAlign),
|
||||||
colorSchema: layout.HeaderColorSchema.color,
|
colorSchema: layout.HeaderColorSchema.color,
|
||||||
@@ -254,62 +289,38 @@ function initializeTransformed ({ $element, component, cubes, layout }) {
|
|||||||
fontSizeAdjustment: getFontSizeAdjustment(layout.lettersize),
|
fontSizeAdjustment: getFontSizeAdjustment(layout.lettersize),
|
||||||
textAlignment: layout.cellTextAlignment
|
textAlignment: layout.cellTextAlignment
|
||||||
},
|
},
|
||||||
semaphoreColors: {
|
conditionalColoring: {
|
||||||
fieldsToApplyTo: {
|
enabled: layout.conditionalcoloring.enabled,
|
||||||
applyToAll: layout.allsemaphores,
|
colorAllRows: layout.conditionalcoloring.colorall,
|
||||||
applyToMetric: layout.allmetrics,
|
rows: layout.conditionalcoloring.rows.map(row => row.rowname),
|
||||||
specificFields: [
|
colorAllMeasures: typeof layout.conditionalcoloring.colorallmeasures === 'undefined'
|
||||||
layout.conceptsemaphore1,
|
|| layout.conditionalcoloring.colorallmeasures,
|
||||||
layout.conceptsemaphore2,
|
measures: !layout.conditionalcoloring.measures
|
||||||
layout.conceptsemaphore3,
|
? [] : layout.conditionalcoloring.measures.split(',').map(index => Number(index)),
|
||||||
layout.conceptsemaphore4,
|
threshold: {
|
||||||
layout.conceptsemaphore5,
|
poor: layout.conditionalcoloring.threshold_poor,
|
||||||
layout.conceptsemaphore6,
|
fair: layout.conditionalcoloring.threshold_fair
|
||||||
layout.conceptsemaphore7,
|
|
||||||
layout.conceptsemaphore9,
|
|
||||||
layout.conceptsemaphore10
|
|
||||||
],
|
|
||||||
metricsSpecificFields: layout.metricssemaphore.split(',').map(entry => Number(entry))
|
|
||||||
},
|
},
|
||||||
status: {
|
colors: {
|
||||||
critical: layout.metricsstatus1,
|
poor: {
|
||||||
medium: layout.metricsstatus2
|
color: layout.conditionalcoloring.color_poor,
|
||||||
},
|
textColor: layout.conditionalcoloring.textcolor_poor
|
||||||
statusColors: {
|
|
||||||
critical: {
|
|
||||||
backgroundColor: layout.colorstatus1.color,
|
|
||||||
color: layout.colorstatus1text.color
|
|
||||||
},
|
},
|
||||||
medium: {
|
fair: {
|
||||||
backgroundColor: layout.colorstatus2.color,
|
color: layout.conditionalcoloring.color_fair,
|
||||||
color: layout.colorstatus2text.color
|
textColor: layout.conditionalcoloring.textcolor_fair
|
||||||
},
|
},
|
||||||
normal: {
|
good: {
|
||||||
backgroundColor: layout.colorstatus3.color,
|
color: layout.conditionalcoloring.color_good,
|
||||||
color: layout.colorstatus3text.color
|
textColor: layout.conditionalcoloring.textcolor_good
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
symbolForNulls: layout.symbolfornulls,
|
symbolForNulls: layout.symbolfornulls,
|
||||||
usePadding: layout.indentbool,
|
usePadding: layout.indentbool
|
||||||
useSeparatorColumns: dimensionCount === 1 ? false : layout.separatorcols
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (rowCount > lastRow && rowCount <= (maxLoops * 1000)) {
|
|
||||||
const requestPage = [
|
|
||||||
{
|
|
||||||
qHeight: Math.min(1000, rowCount - lastRow),
|
|
||||||
qLeft: 0,
|
|
||||||
qTop: matrix.length,
|
|
||||||
qWidth: 10 // should be # of columns
|
|
||||||
}
|
|
||||||
];
|
|
||||||
component.backendApi.getData(requestPage).then(() => {
|
|
||||||
component.paint($element, layout);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return transformedProperties;
|
return transformedProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class LinkedScrollWrapper extends React.PureComponent {
|
|||||||
unlinkComponent (component) {
|
unlinkComponent (component) {
|
||||||
const componentIndex = this.scrollElements.map(element => element.component).indexOf(component);
|
const componentIndex = this.scrollElements.map(element => element.component).indexOf(component);
|
||||||
if (componentIndex !== -1) {
|
if (componentIndex !== -1) {
|
||||||
this.scrollElements.removeAt(componentIndex);
|
this.scrollElements.splice(componentIndex, 1);
|
||||||
// eslint-disable-next-line react/no-find-dom-node
|
// eslint-disable-next-line react/no-find-dom-node
|
||||||
const node = ReactDOM.findDOMNode(component);
|
const node = ReactDOM.findDOMNode(component);
|
||||||
node.onscroll = null;
|
node.onscroll = null;
|
||||||
|
|||||||
229
src/main.less
229
src/main.less
@@ -1,6 +1,5 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
.qv-object-qlik-smart-pivot {
|
.qv-object-qlik-smart-pivot {
|
||||||
@TableBorder: 1px solid #d3d3d3;
|
|
||||||
@KpiTableWidth: 230px;
|
@KpiTableWidth: 230px;
|
||||||
|
|
||||||
*,
|
*,
|
||||||
@@ -13,9 +12,7 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
._cell(@Width: 50px) {
|
.grid-cells {
|
||||||
min-width: @Width !important;
|
|
||||||
max-width: @Width !important;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
line-height: 1em !important;
|
line-height: 1em !important;
|
||||||
}
|
}
|
||||||
@@ -33,18 +30,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
border-collapse: collapse;
|
border-collapse: separate;
|
||||||
border-spacing: 0;
|
border-spacing: 1px;
|
||||||
width: auto;
|
width: auto;
|
||||||
border-left: @TableBorder;
|
|
||||||
border-right: @TableBorder;
|
|
||||||
border-top: @TableBorder;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tr {
|
||||||
|
height: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
td,
|
td,
|
||||||
th {
|
th {
|
||||||
border: 1px solid #fff;
|
|
||||||
border-collapse: collapse;
|
|
||||||
padding: 5px !important; // prevent overwriting from single object
|
padding: 5px !important; // prevent overwriting from single object
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -61,115 +57,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
width: 3%;
|
|
||||||
background: #fff;
|
background: #fff;
|
||||||
min-width: 4px !important;
|
padding: 0 !important;
|
||||||
max-width: 4px !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
th.main-kpi {
|
th.main-kpi {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
border-bottom: @TableBorder;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.numeric {
|
.numeric {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is for wrap text in headers
|
|
||||||
.wrapclass25 {
|
|
||||||
width: 100%;
|
|
||||||
height: inherit;
|
|
||||||
white-space: pre-line;
|
|
||||||
overflow: hidden;
|
|
||||||
display: block;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrapclass45 {
|
|
||||||
width: 100%;
|
|
||||||
height: 45px;
|
|
||||||
white-space: pre-line;
|
|
||||||
overflow: hidden;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrapclass70 {
|
|
||||||
width: 100%;
|
|
||||||
height: 70px;
|
|
||||||
white-space: pre-line;
|
|
||||||
overflow: hidden;
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
line-height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrapclassEmpty {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
// *****************
|
|
||||||
// Medium column size
|
|
||||||
// *****************
|
|
||||||
|
|
||||||
.grid-cells {
|
|
||||||
position: relative;
|
|
||||||
._cell(70px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-cells2 {
|
|
||||||
._cell(70px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-cells-small {
|
|
||||||
._cell(52px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-cells2-small {
|
|
||||||
._cell(52px);
|
|
||||||
}
|
|
||||||
|
|
||||||
// *****************
|
|
||||||
// Small column size
|
|
||||||
// *****************
|
|
||||||
.grid-cells-s {
|
|
||||||
._cell(67px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-cells2-s {
|
|
||||||
._cell(67px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-cells-small-s {
|
|
||||||
._cell(52px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-cells2-small-s {
|
|
||||||
._cell(52px);
|
|
||||||
}
|
|
||||||
|
|
||||||
// *****************
|
|
||||||
// Large column size
|
|
||||||
// *****************
|
|
||||||
.grid-cells-l {
|
|
||||||
._cell(82px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-cells2-l {
|
|
||||||
._cell(82px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-cells-small-l {
|
|
||||||
._cell(66px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-cells2-small-l {
|
|
||||||
._cell(66px);
|
|
||||||
}
|
|
||||||
|
|
||||||
// END OF GRID CELLS
|
|
||||||
|
|
||||||
// First Column
|
// First Column
|
||||||
.fdim-cells {
|
.fdim-cells {
|
||||||
min-width: 230px !Important;
|
min-width: 230px !Important;
|
||||||
@@ -178,25 +78,12 @@
|
|||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fdim-cells:hover {
|
tbody tr:hover td {
|
||||||
background-color: #808080 !important;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody tr:hover {
|
|
||||||
cursor: default;
|
cursor: default;
|
||||||
background-color: #808080 !important;
|
background-color: #808080 !important;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-cells-header {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-cells-title {
|
|
||||||
min-width: 522px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid {
|
.grid {
|
||||||
height: 50px;
|
height: 50px;
|
||||||
width: 350px;
|
width: 350px;
|
||||||
@@ -210,33 +97,64 @@
|
|||||||
border: groove;
|
border: groove;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.root {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
position: absolute; /*Define position */
|
||||||
|
width: 100%; /* Full width (cover the whole page) */
|
||||||
|
height: 100%; /* Full height (cover the whole page) */
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 1000; /* Specify a stack order in case you're using a different order for other elements */
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.kpi-table .fdim-cells,
|
.kpi-table .fdim-cells,
|
||||||
.data-table td {
|
.data-table td {
|
||||||
line-height: 1em !important;
|
line-height: 1em !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-table .fdim-cells {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.kpi-table {
|
.kpi-table {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
flex: none;
|
||||||
width: @KpiTableWidth !important;
|
width: @KpiTableWidth !important;
|
||||||
overflow: hidden !important;
|
|
||||||
height: 100%;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
.header-wrapper {
|
||||||
left: 0;
|
flex: none;
|
||||||
border-right: 1px solid #fff;
|
box-shadow: 4px 2px 8px #e1e1e1;
|
||||||
box-shadow: 4px 2px 8px #e1e1e1;
|
}
|
||||||
|
|
||||||
.row-wrapper {
|
.row-wrapper {
|
||||||
height: calc(~"100% - 97px");
|
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
position: absolute;
|
margin: 0;
|
||||||
|
margin-bottom: 8px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin-top: 0;
|
box-shadow: 4px 2px 8px #e1e1e1;
|
||||||
|
min-height: 0; /* This is to make flex size-filling work */
|
||||||
|
|
||||||
|
/* Adapt for Edge */
|
||||||
|
@supports (-ms-ime-align: auto) {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Adapt for IE11 */
|
||||||
|
@media screen and (-ms-high-contrast: none) {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,27 +163,50 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.data-table {
|
.data-table {
|
||||||
height: 100%;
|
display: flex;
|
||||||
width: calc(100% - 243px);
|
flex-direction: column;
|
||||||
position: absolute;
|
flex-wrap: nowrap;
|
||||||
margin-left: @KpiTableWidth + 13px;
|
margin-left: 13px;
|
||||||
|
min-width: 0; /* This is to make flex size-filling work */
|
||||||
|
|
||||||
.header-wrapper {
|
.header-wrapper {
|
||||||
|
flex: none;
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
width: 100%;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row-wrapper {
|
.row-wrapper {
|
||||||
height: calc(~"100% - 97px");
|
|
||||||
width: 100%;
|
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin-top: 0;
|
min-height: 0; /* This is to make flex size-filling work */
|
||||||
|
|
||||||
|
/* Style scrollbar for FF */
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #d3d3d3 transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Adapt for Edge */
|
||||||
|
@supports (-ms-ime-align: auto) {
|
||||||
|
.header-wrapper {
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Adapt for IE11 */
|
||||||
|
@media screen and (-ms-high-contrast: none) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.header-wrapper {
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// hide scrollbars
|
// hide scrollbars
|
||||||
.kpi-table .header-wrapper,
|
.kpi-table .header-wrapper,
|
||||||
|
.kpi-table .row-wrapper,
|
||||||
.data-table .header-wrapper {
|
.data-table .header-wrapper {
|
||||||
// stylelint-disable-next-line property-no-unknown
|
// stylelint-disable-next-line property-no-unknown
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
|
|||||||
132
src/masking.js
132
src/masking.js
@@ -1,132 +0,0 @@
|
|||||||
import { addSeparators } from './utilities';
|
|
||||||
|
|
||||||
export function ApplyPreMask (mask, value) { // aqui
|
|
||||||
if (mask.indexOf(';') >= 0) {
|
|
||||||
if (value >= 0) {
|
|
||||||
switch (mask.substring(0, mask.indexOf(';'))) {
|
|
||||||
case '#.##0':
|
|
||||||
return (addSeparators(value, '.', ',', 0));
|
|
||||||
case '#,##0':
|
|
||||||
return (addSeparators(value, ',', '.', 0));
|
|
||||||
case '+#.##0':
|
|
||||||
return (addSeparators(value, '.', ',', 0));
|
|
||||||
case '+#,##0':
|
|
||||||
return (addSeparators(value, ',', '.', 0));
|
|
||||||
default:
|
|
||||||
return (applyMask(mask.substring(0, mask.indexOf(';')), value));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const vMyValue = value * -1;
|
|
||||||
let vMyMask = mask.substring(mask.indexOf(';') + 1, mask.length);
|
|
||||||
vMyMask = vMyMask.replace('(', '');
|
|
||||||
vMyMask = vMyMask.replace(')', '');
|
|
||||||
switch (vMyMask) {
|
|
||||||
case '#.##0':
|
|
||||||
return (`(${addSeparators(vMyValue, '.', ',', 0)})`);
|
|
||||||
case '#,##0':
|
|
||||||
return (`(${addSeparators(vMyValue, ',', '.', 0)})`);
|
|
||||||
case '-#.##0':
|
|
||||||
return (`(${addSeparators(vMyValue, '.', ',', 0)})`);
|
|
||||||
case '-#,##0':
|
|
||||||
return (`(${addSeparators(vMyValue, ',', '.', 0)})`);
|
|
||||||
default:
|
|
||||||
return (`(${applyMask(vMyMask, vMyValue)})`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return (applyMask(mask, value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyMask (originalMask, originalValue) {
|
|
||||||
if (!originalMask || isNaN(Number(originalValue))) {
|
|
||||||
return originalValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let isNegative;
|
|
||||||
let result;
|
|
||||||
let integer;
|
|
||||||
// find prefix/suffix
|
|
||||||
let len = originalMask.length;
|
|
||||||
const start = originalMask.search(/[0-9\-\+#]/);
|
|
||||||
const prefix = start > 0 ? originalMask.substring(0, start) : '';
|
|
||||||
// reverse string: not an ideal method if there are surrogate pairs
|
|
||||||
let str = originalMask.split('')
|
|
||||||
.reverse()
|
|
||||||
.join('');
|
|
||||||
const end = str.search(/[0-9\-\+#]/);
|
|
||||||
let offset = len - end;
|
|
||||||
const substr = originalMask.substring(offset, offset + 1);
|
|
||||||
let index = offset + ((substr === '.' || (substr === ',')) ? 1 : 0);
|
|
||||||
const suffix = end > 0 ? originalMask.substring(index, len) : '';
|
|
||||||
|
|
||||||
// mask with prefix & suffix removed
|
|
||||||
let mask = originalMask.substring(start, index);
|
|
||||||
|
|
||||||
// convert any string to number according to formation sign.
|
|
||||||
let value = mask.charAt(0) === '-' ? -originalValue : Number(originalValue);
|
|
||||||
isNegative = value < 0 ? value = -value : 0; // process only abs(), and turn on flag.
|
|
||||||
|
|
||||||
// search for separator for grp & decimal, anything not digit, not +/- sign, not #.
|
|
||||||
result = mask.match(/[^\d\-\+#]/g);
|
|
||||||
const decimal = (result && result[result.length - 1]) || '.'; // treat the right most symbol as decimal
|
|
||||||
const group = (result && result[1] && result[0]) || ','; // treat the left most symbol as group separator
|
|
||||||
|
|
||||||
// split the decimal for the format string if any.
|
|
||||||
mask = mask.split(decimal);
|
|
||||||
// Fix the decimal first, toFixed will auto fill trailing zero.
|
|
||||||
value = value.toFixed(mask[1] && mask[1].length);
|
|
||||||
value = String(Number(value)); // convert number to string to trim off *all* trailing decimal zero(es)
|
|
||||||
|
|
||||||
// fill back any trailing zero according to format
|
|
||||||
const posTrailZero = mask[1] && mask[1].lastIndexOf('0'); // look for last zero in format
|
|
||||||
const part = value.split('.');
|
|
||||||
// integer will get !part[1]
|
|
||||||
if (!part[1] || (part[1] && part[1].length <= posTrailZero)) {
|
|
||||||
value = (Number(value)).toFixed(posTrailZero + 1);
|
|
||||||
}
|
|
||||||
const szSep = mask[0].split(group); // look for separator
|
|
||||||
mask[0] = szSep.join(''); // join back without separator for counting the pos of any leading 0.
|
|
||||||
|
|
||||||
const posLeadZero = mask[0] && mask[0].indexOf('0');
|
|
||||||
if (posLeadZero > -1) {
|
|
||||||
while (part[0].length < (mask[0].length - posLeadZero)) {
|
|
||||||
part[0] = `0${part[0]}`;
|
|
||||||
}
|
|
||||||
} else if (Number(part[0]) === 0) {
|
|
||||||
part[0] = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
value = value.split('.');
|
|
||||||
value[0] = part[0];
|
|
||||||
|
|
||||||
// process the first group separator from decimal (.) only, the rest ignore.
|
|
||||||
// get the length of the last slice of split result.
|
|
||||||
const posSeparator = (szSep[1] && szSep[szSep.length - 1].length);
|
|
||||||
if (posSeparator) {
|
|
||||||
integer = value[0];
|
|
||||||
str = '';
|
|
||||||
offset = integer.length % posSeparator;
|
|
||||||
len = integer.length;
|
|
||||||
for (index = 0; index < len; index++) {
|
|
||||||
str += integer.charAt(index); // ie6 only support charAt for sz.
|
|
||||||
// -posSeparator so that won't trail separator on full length
|
|
||||||
// jshint -W018
|
|
||||||
if (!((index - offset + 1) % posSeparator) && index < len - posSeparator) {
|
|
||||||
str += group;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
value[0] = str;
|
|
||||||
}
|
|
||||||
value[1] = (mask[1] && value[1]) ? decimal + value[1] : '';
|
|
||||||
|
|
||||||
// remove negative sign if result is zero
|
|
||||||
result = value.join('');
|
|
||||||
if (result === '0' || result === '') {
|
|
||||||
// remove negative sign if result is zero
|
|
||||||
isNegative = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// put back any negation, combine integer and fraction, and add back prefix & suffix
|
|
||||||
return prefix + ((isNegative ? '-' : '') + result) + suffix;
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import initializeStore from './store';
|
|
||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import Root from './root.jsx';
|
|
||||||
import { initializeCubes } from './dataset';
|
|
||||||
|
|
||||||
export default async function paint ($element, layout, component) {
|
|
||||||
const cubes = await initializeCubes({
|
|
||||||
component,
|
|
||||||
layout
|
|
||||||
});
|
|
||||||
const state = await initializeStore({
|
|
||||||
$element,
|
|
||||||
component,
|
|
||||||
cubes,
|
|
||||||
layout
|
|
||||||
});
|
|
||||||
const editmodeClass = component.inAnalysisState() ? '' : 'edit-mode';
|
|
||||||
const jsx = (
|
|
||||||
<Root
|
|
||||||
qlik={component}
|
|
||||||
state={state}
|
|
||||||
editmodeClass={editmodeClass}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
ReactDOM.render(jsx, $element[0]);
|
|
||||||
}
|
|
||||||
189
src/root.jsx
189
src/root.jsx
@@ -1,60 +1,143 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from "prop-types";
|
||||||
import HeadersTable from './headers-table/index.jsx';
|
import HeadersTable from "./headers-table/index.jsx";
|
||||||
import DataTable from './data-table/index.jsx';
|
import DataTable from "./data-table/index.jsx";
|
||||||
import { LinkedScrollWrapper, LinkedScrollSection } from './linked-scroll';
|
import { LinkedScrollWrapper, LinkedScrollSection } from "./linked-scroll";
|
||||||
|
|
||||||
const Root = ({ state, qlik, editmodeClass }) => (
|
class Root extends React.PureComponent {
|
||||||
<LinkedScrollWrapper>
|
constructor (props) {
|
||||||
<div className={`kpi-table ${editmodeClass}`}>
|
super(props);
|
||||||
<HeadersTable
|
this.onDataTableRefSet = this.onDataTableRefSet.bind(this);
|
||||||
data={state.data}
|
this.renderedTableWidth = 0;
|
||||||
general={state.general}
|
}
|
||||||
qlik={qlik}
|
|
||||||
styling={state.styling}
|
componentDidUpdate () {
|
||||||
/>
|
let tableWidth;
|
||||||
<LinkedScrollSection linkVertical>
|
if (this.dataTableRef) {
|
||||||
<DataTable
|
tableWidth = this.dataTableRef.getBoundingClientRect().width;
|
||||||
data={state.data}
|
if (this.renderedTableWidth !== tableWidth) {
|
||||||
general={state.general}
|
this.forceUpdate();
|
||||||
qlik={qlik}
|
}
|
||||||
renderData={false}
|
}
|
||||||
styling={state.styling}
|
}
|
||||||
/>
|
|
||||||
</LinkedScrollSection>
|
onDataTableRefSet (element) {
|
||||||
</div>
|
this.dataTableRef = element;
|
||||||
<div className={`data-table ${editmodeClass}`}>
|
this.forceUpdate();
|
||||||
<LinkedScrollSection linkHorizontal>
|
}
|
||||||
<HeadersTable
|
|
||||||
data={state.data}
|
render () {
|
||||||
general={state.general}
|
const { editmodeClass, component, state } = this.props;
|
||||||
qlik={qlik}
|
const { data, general, styling, error } = state;
|
||||||
styling={state.styling}
|
|
||||||
/>
|
// Determine cell- and column separator width
|
||||||
</LinkedScrollSection>
|
let cellWidth = '0px';
|
||||||
<LinkedScrollSection
|
let columnSeparatorWidth = '';
|
||||||
linkHorizontal
|
if (this.dataTableRef) {
|
||||||
linkVertical
|
const tableWidth = this.dataTableRef.getBoundingClientRect().width;
|
||||||
>
|
this.renderedTableWidth = tableWidth;
|
||||||
<DataTable
|
|
||||||
data={state.data}
|
if (general.cellWidth) {
|
||||||
general={state.general}
|
cellWidth = general.cellWidth;
|
||||||
qlik={qlik}
|
if (general.useColumnSeparator) {
|
||||||
styling={state.styling}
|
columnSeparatorWidth = '8px';
|
||||||
/>
|
}
|
||||||
</LinkedScrollSection>
|
} else {
|
||||||
</div>
|
const headerMarginRight = 8;
|
||||||
</LinkedScrollWrapper>
|
const borderWidth = 1;
|
||||||
);
|
const rowCellCount = data.matrix[0].length;
|
||||||
|
|
||||||
|
let separatorCount = 0;
|
||||||
|
let separatorWidth = 0;
|
||||||
|
if (general.useColumnSeparator) {
|
||||||
|
separatorCount = data.headers.dimension2.length - 1;
|
||||||
|
separatorWidth = Math.min(
|
||||||
|
Math.floor((tableWidth * 0.2) / separatorCount),
|
||||||
|
8
|
||||||
|
);
|
||||||
|
columnSeparatorWidth = `${separatorWidth}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const separatorWidthSum =
|
||||||
|
(separatorWidth + borderWidth) * separatorCount;
|
||||||
|
cellWidth = `${Math.floor(
|
||||||
|
(tableWidth - separatorWidthSum - headerMarginRight - borderWidth) /
|
||||||
|
rowCellCount) - borderWidth}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="root">
|
||||||
|
{error ? (
|
||||||
|
<div className={`error ${editmodeClass}`}>
|
||||||
|
{state.layout.errormessage}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<LinkedScrollWrapper>
|
||||||
|
<div className={`kpi-table ${editmodeClass}`}>
|
||||||
|
<HeadersTable
|
||||||
|
cellWidth={cellWidth}
|
||||||
|
columnSeparatorWidth={columnSeparatorWidth}
|
||||||
|
component={component}
|
||||||
|
data={data}
|
||||||
|
general={general}
|
||||||
|
isKpi
|
||||||
|
styling={styling}
|
||||||
|
/>
|
||||||
|
<LinkedScrollSection linkVertical>
|
||||||
|
<DataTable
|
||||||
|
cellWidth={cellWidth}
|
||||||
|
columnSeparatorWidth={columnSeparatorWidth}
|
||||||
|
component={component}
|
||||||
|
data={data}
|
||||||
|
general={general}
|
||||||
|
renderData={false}
|
||||||
|
styling={styling}
|
||||||
|
/>
|
||||||
|
</LinkedScrollSection>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`data-table ${editmodeClass}`}
|
||||||
|
style={{ width: general.cellWidth ? 'auto' : '100%' }}
|
||||||
|
ref={this.onDataTableRefSet}
|
||||||
|
>
|
||||||
|
<LinkedScrollSection linkHorizontal>
|
||||||
|
<HeadersTable
|
||||||
|
cellWidth={cellWidth}
|
||||||
|
columnSeparatorWidth={columnSeparatorWidth}
|
||||||
|
component={component}
|
||||||
|
data={data}
|
||||||
|
general={general}
|
||||||
|
isKpi={false}
|
||||||
|
styling={styling}
|
||||||
|
/>
|
||||||
|
</LinkedScrollSection>
|
||||||
|
<LinkedScrollSection linkHorizontal linkVertical>
|
||||||
|
<DataTable
|
||||||
|
cellWidth={cellWidth}
|
||||||
|
columnSeparatorWidth={columnSeparatorWidth}
|
||||||
|
component={component}
|
||||||
|
data={data}
|
||||||
|
general={general}
|
||||||
|
styling={styling}
|
||||||
|
/>
|
||||||
|
</LinkedScrollSection>
|
||||||
|
</div>
|
||||||
|
</LinkedScrollWrapper>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Root.propTypes = {
|
Root.propTypes = {
|
||||||
qlik: PropTypes.shape({}).isRequired,
|
component: PropTypes.shape({}).isRequired,
|
||||||
|
editmodeClass: PropTypes.string.isRequired,
|
||||||
state: PropTypes.shape({
|
state: PropTypes.shape({
|
||||||
data: PropTypes.object.isRequired,
|
data: PropTypes.object,
|
||||||
general: PropTypes.object.isRequired,
|
general: PropTypes.object,
|
||||||
styling: PropTypes.object.isRequired
|
styling: PropTypes.object
|
||||||
}).isRequired,
|
}).isRequired
|
||||||
editmodeClass: PropTypes.string.isRequired
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Root;
|
export default Root;
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import initializeTransformed from './initialize-transformed';
|
import initializeTransformed from './initialize-transformed';
|
||||||
|
|
||||||
async function initialize ({ $element, layout, component, cubes }) {
|
async function initialize ({ $element, layout, component, dataCube, designList }) {
|
||||||
const transformedProperties = await initializeTransformed({
|
const transformedProperties = await initializeTransformed({
|
||||||
$element,
|
$element,
|
||||||
component,
|
component,
|
||||||
cubes,
|
dataCube,
|
||||||
|
designList,
|
||||||
layout
|
layout
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -25,23 +25,21 @@ class Tooltip extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { children, tooltipText } = this.props;
|
const { children, styling, tooltipText } = this.props;
|
||||||
const { showTooltip } = this.state;
|
const { showTooltip } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onMouseMove={handleCalculateTooltipPosition}
|
onMouseMove={handleCalculateTooltipPosition}
|
||||||
onMouseOut={this.handleRenderTooltip}
|
onMouseOut={this.handleRenderTooltip}
|
||||||
onMouseOver={this.handleRenderTooltip}
|
onMouseOver={this.handleRenderTooltip}
|
||||||
|
style={{ fontFamily: styling.options.fontFamily }}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
{showTooltip
|
{showTooltip
|
||||||
? (
|
? (
|
||||||
<div
|
<div className="tooltip-wrapper">
|
||||||
className="tooltip-wrapper"
|
<p style={{ fontFamily: styling.options.fontFamily }}>
|
||||||
>
|
|
||||||
<p>
|
|
||||||
{tooltipText}
|
{tooltipText}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -56,6 +54,11 @@ Tooltip.propTypes = {
|
|||||||
PropTypes.arrayOf(PropTypes.node),
|
PropTypes.arrayOf(PropTypes.node),
|
||||||
PropTypes.node
|
PropTypes.node
|
||||||
]).isRequired,
|
]).isRequired,
|
||||||
|
styling: PropTypes.shape({
|
||||||
|
options: PropTypes.shape({
|
||||||
|
fontFamily: PropTypes.string.isRequired
|
||||||
|
}).isRequired
|
||||||
|
}).isRequired,
|
||||||
tooltipText: PropTypes.string.isRequired
|
tooltipText: PropTypes.string.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,21 +9,6 @@ export function distinctArray (array) {
|
|||||||
.map(entry => JSON.parse(entry));
|
.map(entry => JSON.parse(entry));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addSeparators (number, thousandSeparator, decimalSeparator, numberOfDecimals) {
|
|
||||||
const numberString = number.toFixed(numberOfDecimals);
|
|
||||||
const numberStringParts = numberString.split('.');
|
|
||||||
let [
|
|
||||||
wholeNumber,
|
|
||||||
decimal
|
|
||||||
] = numberStringParts;
|
|
||||||
decimal = numberStringParts.length > 1 ? decimalSeparator + decimal : '';
|
|
||||||
const regexCheckForThousand = /(\d+)(\d{3})/;
|
|
||||||
while (regexCheckForThousand.test(wholeNumber)) {
|
|
||||||
wholeNumber = wholeNumber.replace(regexCheckForThousand, `$1${thousandSeparator}$2`);
|
|
||||||
}
|
|
||||||
return wholeNumber + decimal;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Deferred () {
|
export function Deferred () {
|
||||||
this.promise = new Promise((resolve, reject) => {
|
this.promise = new Promise((resolve, reject) => {
|
||||||
this.resolve = resolve;
|
this.resolve = resolve;
|
||||||
@@ -31,7 +16,7 @@ export function Deferred () {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function injectSeparators (array, shouldHaveSeparator, suppliedOptions) {
|
export function injectSeparators (array, columnSeparatorWidth, suppliedOptions) {
|
||||||
const defaultOptions = {
|
const defaultOptions = {
|
||||||
atEvery: 1,
|
atEvery: 1,
|
||||||
separator: { isSeparator: true }
|
separator: { isSeparator: true }
|
||||||
@@ -41,7 +26,7 @@ export function injectSeparators (array, shouldHaveSeparator, suppliedOptions) {
|
|||||||
...suppliedOptions
|
...suppliedOptions
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!shouldHaveSeparator) {
|
if (!columnSeparatorWidth) {
|
||||||
return array;
|
return array;
|
||||||
}
|
}
|
||||||
return array.reduce((result, entry, index) => {
|
return array.reduce((result, entry, index) => {
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ module.exports = {
|
|||||||
'indentation': 2,
|
'indentation': 2,
|
||||||
'length-zero-no-unit': true,
|
'length-zero-no-unit': true,
|
||||||
'max-empty-lines': 1,
|
'max-empty-lines': 1,
|
||||||
'max-nesting-depth': 3,
|
'max-nesting-depth': 5,
|
||||||
'media-feature-colon-space-after': 'always',
|
'media-feature-colon-space-after': 'always',
|
||||||
'media-feature-colon-space-before': 'never',
|
'media-feature-colon-space-before': 'never',
|
||||||
'media-feature-name-case': 'lower',
|
'media-feature-name-case': 'lower',
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,10 +0,0 @@
|
|||||||
import Enzyme from 'enzyme';
|
|
||||||
import Adapter from 'enzyme-adapter-react-16';
|
|
||||||
import jasmineEnzyme from 'jasmine-enzyme';
|
|
||||||
Enzyme.configure({ adapter: new Adapter() });
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jasmineEnzyme();
|
|
||||||
});
|
|
||||||
|
|
||||||
export * from 'enzyme';
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { mount } from 'test-utilities/enzyme-setup';
|
|
||||||
|
|
||||||
export function mountedComponent (Model, Component, props = {}) {
|
|
||||||
const component = mount((
|
|
||||||
<Component {...props} />
|
|
||||||
)).find(Component);
|
|
||||||
return new Model(component);
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
|
||||||
const StyleLintPlugin = require('stylelint-webpack-plugin');
|
const StyleLintPlugin = require('stylelint-webpack-plugin');
|
||||||
const settings = require('./settings');
|
const packageJSON = require('./package.json');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
console.log('Webpack mode:', settings.mode); // eslint-disable-line no-console
|
const DIST = path.resolve("./dist");
|
||||||
|
const MODE = process.env.NODE_ENV || 'development';
|
||||||
|
|
||||||
|
console.log('Webpack mode:', MODE); // eslint-disable-line no-console
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
devtool: 'source-map',
|
devtool: 'source-map',
|
||||||
@@ -21,7 +24,7 @@ const config = {
|
|||||||
root: '_'
|
root: '_'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mode: settings.mode,
|
mode: MODE,
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
@@ -61,17 +64,11 @@ const config = {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
filename: `${settings.name}.js`,
|
filename: `${packageJSON.name}.js`,
|
||||||
libraryTarget: 'amd',
|
libraryTarget: 'amd',
|
||||||
path: settings.buildDestination
|
path: DIST
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new CopyWebpackPlugin([
|
|
||||||
`assets/${settings.name}.qext`,
|
|
||||||
`assets/${settings.name}.png`,
|
|
||||||
'assets/wbfolder.wbl',
|
|
||||||
'resources/Excel.png'
|
|
||||||
], {}),
|
|
||||||
new StyleLintPlugin({
|
new StyleLintPlugin({
|
||||||
files: '**/*.less'
|
files: '**/*.less'
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user