Compare commits
179 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
769d5cfa3f | ||
|
|
9d925b6205 | ||
|
|
34527c3d6d | ||
|
|
35d489c2e2 | ||
|
|
9ef9981305 | ||
|
|
398192e057 | ||
|
|
4421217cb0 | ||
|
|
c92be00ca7 | ||
|
|
c66dfdc06c | ||
|
|
a882b1d6aa | ||
|
|
554b029569 | ||
|
|
4a15628325 | ||
|
|
fdb2aaaef4 | ||
|
|
a400e9c233 | ||
|
|
091c564a75 | ||
|
|
99eba8afcb | ||
|
|
cb78a2f2f9 | ||
|
|
f255efbf5d | ||
|
|
2f2d08fedd | ||
|
|
ac8b70bc84 | ||
|
|
0f2a4f9805 | ||
|
|
85228412cc | ||
|
|
03dfc0ce93 | ||
|
|
48427df559 | ||
|
|
7fda7aa2d9 | ||
|
|
4ba12b8b2d | ||
|
|
07af7b509e | ||
|
|
b7ff83e1da | ||
|
|
621359d6f9 | ||
|
|
a71f80f8fa | ||
|
|
a6cbfcda70 | ||
|
|
de2e9c16ac | ||
|
|
ad0c0dacba | ||
|
|
97564cf8b1 | ||
|
|
7fa44c06b0 | ||
|
|
dae192b7af | ||
|
|
5abfd5b7e5 | ||
|
|
65f5d70654 | ||
|
|
da7ba5c3a8 | ||
|
|
6e5df323d8 | ||
|
|
aad92678db | ||
|
|
15226d8598 | ||
|
|
a5fc586859 | ||
|
|
980c0387bf | ||
|
|
710d4a8842 | ||
|
|
ebb5a7cf16 | ||
|
|
7107f485be | ||
|
|
633fd39b80 | ||
|
|
9eeaecb994 | ||
|
|
7305175049 | ||
|
|
f99281ff47 | ||
|
|
557cd1aeb6 | ||
|
|
ca5f442fe0 | ||
|
|
585243bb73 | ||
|
|
a25b2c40c0 | ||
|
|
46d6520273 | ||
|
|
19286f6c56 | ||
|
|
8b760646ab | ||
|
|
b5f25e5e18 | ||
|
|
b335b4883e | ||
|
|
94e4203a0b | ||
|
|
221e2d365c | ||
|
|
6797f7d561 | ||
|
|
951d534343 | ||
|
|
c5acb43a7a | ||
|
|
979c036b49 | ||
|
|
63c877face | ||
|
|
9809587c68 | ||
|
|
bbadc711dc | ||
|
|
18e2b2024e | ||
|
|
807a609a90 | ||
|
|
82257be3a8 | ||
|
|
734fe33537 | ||
|
|
347e6b7408 | ||
|
|
bdf231f88d | ||
|
|
88ad73bd41 | ||
|
|
530f0919f1 | ||
|
|
79b89a3b25 | ||
|
|
4520d6a48a | ||
|
|
98678d4a13 | ||
|
|
da57204c41 | ||
|
|
906a11c6b4 | ||
|
|
c66ad78e48 | ||
|
|
6994bf51a3 | ||
|
|
521d508604 | ||
|
|
3946f6c894 | ||
|
|
aeccbf5d17 | ||
|
|
95e330a50d | ||
|
|
27b84c5623 | ||
|
|
a804c31658 | ||
|
|
9efe580d18 | ||
|
|
fa60dd5248 | ||
|
|
fd653de0e1 | ||
|
|
48970cb4f4 | ||
|
|
fe4b5a72ec | ||
|
|
8b41022ddc | ||
|
|
9c66c09899 | ||
|
|
09d9055643 | ||
|
|
b1204e0929 | ||
|
|
d130ca266d | ||
|
|
f730dc2827 | ||
|
|
b5f74395f7 | ||
|
|
dfac9ad5e9 | ||
|
|
377d642fe2 | ||
|
|
8984affe87 | ||
|
|
ce1e196b78 | ||
|
|
e3b7a7640e | ||
|
|
68dccf8575 | ||
|
|
8e100f286b | ||
|
|
f7ceb5c2bf |
@@ -26,9 +26,6 @@ jobs:
|
||||
--blackduck.username="svc-blackduck" \
|
||||
--blackduck.password=${svc_blackduck} \
|
||||
--detect.project.name="viz-bundle-qlik-smart-pivot"
|
||||
- run:
|
||||
name: Run tests
|
||||
command: npm run test-once
|
||||
|
||||
bump-version:
|
||||
<<: *defaults
|
||||
@@ -56,16 +53,18 @@ jobs:
|
||||
command: |
|
||||
export VERSION=$(scripts/get-bumped-version.sh)
|
||||
echo "Version: ${VERSION}"
|
||||
npm run build
|
||||
npm run build:zip
|
||||
sudo chmod +x scripts/verify-files.sh
|
||||
scripts/verify-files.sh
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
- persist_to_workspace:
|
||||
root: ~/qlik-smart-pivot
|
||||
paths:
|
||||
- build
|
||||
- dist
|
||||
- store_artifacts:
|
||||
path: build
|
||||
destination: build
|
||||
path: dist
|
||||
destination: dist
|
||||
deploy:
|
||||
<<: *defaults
|
||||
steps:
|
||||
|
||||
20
.eslintrc.js
20
.eslintrc.js
@@ -15,6 +15,7 @@ module.exports = {
|
||||
},
|
||||
globals: {
|
||||
angular: false,
|
||||
beforeEach: false,
|
||||
define: false,
|
||||
describe: false,
|
||||
document: false,
|
||||
@@ -41,12 +42,12 @@ module.exports = {
|
||||
"no-cond-assign": ["warn"],
|
||||
"no-fallthrough": ["warn"],
|
||||
"no-undef": ["error"],
|
||||
"no-unused-vars": ["warn"],
|
||||
"no-use-before-define": ["warn", { "functions": false, "classes": false, "variables": false }],
|
||||
"no-unused-vars": ["error"],
|
||||
"no-use-before-define": ["error", { "functions": false, "classes": false, "variables": false }],
|
||||
"no-useless-escape": ["warn"],
|
||||
"no-useless-return": ["warn"],
|
||||
"no-underscore-dangle": ["warn", { "allow": ["_id"] }],
|
||||
"no-redeclare": ["warn"],
|
||||
"no-redeclare": ["error"],
|
||||
"no-restricted-syntax": ["warn"],
|
||||
"operator-linebreak": ["warn", "before"],
|
||||
"prefer-promise-reject-errors": ["warn"],
|
||||
@@ -63,11 +64,11 @@ module.exports = {
|
||||
"complexity": ["warn"],
|
||||
"camelcase": ["warn"],
|
||||
"max-statements": ["off"], // marks the entire functions, a bit too noisy
|
||||
"sort-vars": ["warn"],
|
||||
"sort-vars": ["off"], // not much value for the work
|
||||
"init-declarations": ["off"],
|
||||
"capitalized-comments": ["off"],
|
||||
"one-var": ["off"],
|
||||
"no-var": ["warn"],
|
||||
"no-var": ["error"],
|
||||
"no-plusplus": ["warn"],
|
||||
"vars-on-top": ["off"],
|
||||
"no-magic-numbers": ["off"], // useful, but also complains for reasonable checks with actual numbers
|
||||
@@ -80,7 +81,7 @@ module.exports = {
|
||||
"quote-props": ["off"],
|
||||
"prefer-template": ["warn"],
|
||||
"no-lonely-if": ["warn"],
|
||||
"sort-keys": ["warn"],
|
||||
"sort-keys": ["off"], // not much value for the work
|
||||
"no-implicit-coercion": ["warn"],
|
||||
"no-inline-comments": ["off"],
|
||||
"spaced-comment": ["warn"],
|
||||
@@ -107,7 +108,7 @@ module.exports = {
|
||||
"strict": ["warn"],
|
||||
"no-ternary": ["off"],
|
||||
"multiline-ternary": ["off"],
|
||||
"no-param-reassign": ["warn"],
|
||||
"no-param-reassign": ["error"],
|
||||
"prefer-destructuring": ["warn"],
|
||||
"arrow-parens": ["off"],
|
||||
"no-array-constructor": ["warn"],
|
||||
@@ -116,7 +117,7 @@ module.exports = {
|
||||
"max-params": ["warn"],
|
||||
"brace-style": ["warn", "1tbs", { "allowSingleLine": true }],
|
||||
"prefer-const": ["warn"],
|
||||
|
||||
"class-methods-use-this":["warn"],
|
||||
// plugin:react
|
||||
"react/jsx-indent": ["warn", 2],
|
||||
"react/jsx-indent-props": ["warn", 2],
|
||||
@@ -129,7 +130,8 @@ module.exports = {
|
||||
"react/jsx-no-literals": ["off"],
|
||||
"react/jsx-max-depth": ["off"], // rule throws exception in single-dimension-measure
|
||||
"react/jsx-filename-extension": ["warn"],
|
||||
"react/prefer-stateless-function": ["warn"]
|
||||
"react/prefer-stateless-function": ["warn"],
|
||||
"react/no-set-state": ["warn"]
|
||||
},
|
||||
extends: [
|
||||
"eslint:all",
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -19,7 +19,7 @@ $RECYCLE.BIN/
|
||||
|
||||
# Temporary build files
|
||||
node_modules/
|
||||
build/
|
||||
dist/
|
||||
BUMPED_VERSION
|
||||
|
||||
# =========================
|
||||
|
||||
49
README.md
49
README.md
@@ -1,57 +1,22 @@
|
||||
# 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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
Usage documentation for the extension is available at https://help.qlik.com.
|
||||
|
||||
# Developing the extension
|
||||
|
||||
If you want to do code changes to the extension follow these simple steps to get going.
|
||||
|
||||
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
|
||||
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>`.
|
||||
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.
|
||||
|
||||
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`.
|
||||
|
||||
4. Run `npm run build` - to build a dev-version to the /dist folder.
|
||||
5. Move the content of the /dist folder to the extension directory. Usually in `C:/Users/<user>/Documents/Qlik/Sense/Extensions/qlik-smart-pivot`.
|
||||
|
||||
# Original authors
|
||||
|
||||
[github.com/iviasensio](https://github.com/iviasensio)
|
||||
|
||||
|
||||
# License
|
||||
|
||||
Released under the [MIT License](LICENSE).
|
||||
Released under the [MIT License](LICENSE).
|
||||
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 123 B After Width: | Height: | Size: 7.5 KiB |
@@ -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 gutil = require('gulp-util');
|
||||
var zip = require('gulp-zip');
|
||||
var del = require('del');
|
||||
var path = require('path');
|
||||
var settings = require('./settings');
|
||||
var webpackConfig = require('./webpack.config');
|
||||
var webpack = require('webpack');
|
||||
var WebpackDevServer = require('webpack-dev-server');
|
||||
var jeditor = require("gulp-json-editor");
|
||||
var pkg = require('./package.json');
|
||||
|
||||
var srcFiles = path.resolve('./src/**/*.*');
|
||||
var DIST = './dist';
|
||||
var VERSION = process.env.VERSION || 'local-dev';
|
||||
|
||||
gulp.task('remove-build-folder', function(){
|
||||
return del([settings.buildDestination], { force: true });
|
||||
gulp.task('qext', function () {
|
||||
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(){
|
||||
return gulp.src(settings.buildDestination + '/**/*')
|
||||
.pipe(zip(`${settings.name}_${settings.version}.zip`))
|
||||
.pipe(gulp.dest(settings.buildDestination));
|
||||
return gulp.src(DIST + '/**/*')
|
||||
.pipe(zip(`${pkg.name}_${VERSION}.zip`))
|
||||
.pipe(gulp.dest(DIST));
|
||||
});
|
||||
|
||||
gulp.task('add-assets', function(){
|
||||
return gulp.src('./assets/**/*').pipe(gulp.dest(DIST));
|
||||
});
|
||||
|
||||
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.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.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,43 +0,0 @@
|
||||
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: false }
|
||||
],
|
||||
frameworks: ['jasmine'],
|
||||
preprocessors: {
|
||||
'src/*.spec.js': ['webpack', 'sourcemap']
|
||||
},
|
||||
webpack: {
|
||||
devtool: 'source-map',
|
||||
mode: settings.mode,
|
||||
externals: {
|
||||
jquery: {
|
||||
amd: 'jquery',
|
||||
commonjs: 'jquery',
|
||||
commonjs2: 'jquery',
|
||||
root: '_'
|
||||
},
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: [/node_modules/],
|
||||
loaders: ['babel-loader']
|
||||
},
|
||||
{ test: /\.less$/, loader: 'ignore-loader' },
|
||||
{ test: /\.json$/, loader: 'ignore-loader' }
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
2871
package-lock.json
generated
2871
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
26
package.json
26
package.json
@@ -1,18 +1,16 @@
|
||||
{
|
||||
"name": "qlik-smart-pivot",
|
||||
"version": "0.0.1",
|
||||
"description": "Formatted table for P&L reports.",
|
||||
"keywords": "smart pivot qliksense extension",
|
||||
"description": "Profit & Loss reporting with color and font customizations.",
|
||||
"homepage": "",
|
||||
"repository": "https://github.com/iviasensio/PLSmartPivot",
|
||||
"author": "Ivan Felipe Asensio <ivan.felipe@qlik.com>",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "gulp build",
|
||||
"build:zip": "gulp zip",
|
||||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.1.2",
|
||||
@@ -23,30 +21,24 @@
|
||||
"@babel/preset-react": "7.0.0",
|
||||
"babel-eslint": "10.0.1",
|
||||
"babel-loader": "8.0.4",
|
||||
"copy-webpack-plugin": "4.5.3",
|
||||
"css-loader": "1.0.0",
|
||||
"del": "2.0.2",
|
||||
"enzyme": "3.8.0",
|
||||
"enzyme-adapter-react-16": "1.9.1",
|
||||
"eslint": "5.7.0",
|
||||
"eslint-loader": "2.1.1",
|
||||
"eslint-plugin-react": "7.11.1",
|
||||
"gulp": "4.0.0",
|
||||
"gulp-json-editor": "2.4.3",
|
||||
"gulp-util": "^3.0.7",
|
||||
"gulp-zip": "3.0.2",
|
||||
"jasmine-core": "3.2.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-loader": "4.1.0",
|
||||
"lodash.merge": "4.6.1",
|
||||
"style-loader": "0.23.1",
|
||||
"stylelint": "8.4.0",
|
||||
"stylelint-webpack-plugin": "0.10.5",
|
||||
"text-loader": "0.0.1",
|
||||
"webpack": "4.20.2",
|
||||
"webpack-cli": "3.1.2",
|
||||
"webpack-dev-server": "3.1.10"
|
||||
"webpack": "4.20.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": "15.6.2",
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
Accounts;Bold;Background;FontStyle;LetterColor;Align;Size;Comment
|
||||
Revenues;;rgb(183, 219, 255);<italic>;;<center>;;<comment>
|
||||
Gross sales revenues;;;;;;;
|
||||
Less return & allowances;;;;;;;
|
||||
Net sales revenues;<bold>;<soft>;;;;;
|
||||
Cost of goods sold;<bold>;;;;;;
|
||||
Direct materials;;;;;;;
|
||||
Direct labor;;;;;;;
|
||||
Manufacturing overhead;;#b7dbff;<italic>;;<center>;;<comment>
|
||||
Indirect labor;;;;;;;
|
||||
Depreciation, manufacturing equip;;;;;;;
|
||||
Other mfr overhead;;;;;;;
|
||||
Net mfr overhead;<bold>;<violete>;;;;;
|
||||
Net costs of goods sold;<bold>;<violete>;;;;;
|
||||
Gross profit;<bold>;<dark>;;;<center>;<large>;
|
||||
Operating expenses;<bold>;;<italic>;;;;
|
||||
Selling expenses;;;;;;;
|
||||
Sales salaries;;;;;;;
|
||||
Warranty expenses;;;;;;;
|
||||
Depreciation, store equipment;;;;;;;
|
||||
Other selling expenses3;;;;;;;
|
||||
Total selling expenses;<bold>;<violete>;;;;;
|
||||
General & administrative expenses;;#b7dbff;<italic>;;<center>;;<comment>
|
||||
Administration salaries;;;;;;;
|
||||
Rent expenses;;;;;;;
|
||||
Depreciation, computers;;;;;;;
|
||||
Other general & admin expenses;;;;;;;
|
||||
total general & admin expenses;<bold>;<soft>;;;;;
|
||||
total operating expenses;<bold>;<violete>;;;;;
|
||||
Operating income before taxes;<bold>;<dark>;;<white>;;<large>;
|
||||
Financial revenue & expenses;;#b7dbff;<italic>;;<center>;;<comment>
|
||||
Revenue from investments;;;;;;;
|
||||
Less interest expenses;;;;;;;
|
||||
Net financial gain (expense);;;;;;;
|
||||
Income before tax & extraordinary items;<bold>;<dark>;;<white>;;<large>;
|
||||
Less income tax on operations;;;;;;;
|
||||
Income before extraordinary items;;;;;;;
|
||||
Extraordinary items;;#b7dbff;<italic>;;<center>;;<comment>
|
||||
Sale of land;;;;;;;
|
||||
Less initial cost;;;;;;;
|
||||
Net gain on sale of land;<bold>;<soft>;;;;;
|
||||
Less income tax on gain;;;;;;;
|
||||
Extraordinary items after tax;<bold>;<soft>;;;;;
|
||||
Net Income (Profit);<bold>;<dark>;;<white>;<center>;<large>;
|
||||
|
@@ -1,21 +0,0 @@
|
||||
Accounts;Bold;Background;FontStyle;LetterColor;Align;Size;Comment
|
||||
Revenues;<bold>;;<italic>;;<center>;;<comment>
|
||||
Net sales revenues;<bold>;rgb(128, 191, 255);;;;;
|
||||
Cost of goods sold;<bold>;rgb(128, 191, 255);;;;;
|
||||
Manufacturing overhead;<bold>;;<italic>;;<center>;;<comment>
|
||||
Net mfr overhead;<bold>;rgb(128, 191, 255);;;;;
|
||||
Net costs of goods sold;<bold>;rgb(128, 191, 255);;;;;
|
||||
Gross profit;<bold>;rgb(0, 102, 204);<white>;;<center>;<large>;
|
||||
Operating expenses;<bold>;;<italic>;;;;
|
||||
Total selling expenses;<bold>;rgb(128, 191, 255);;;;;
|
||||
General & administrative expenses;<bold>;;<italic>;;<center>;;<comment>
|
||||
Other general & admin expenses;<bold>;rgb(128, 191, 255);<white>;;<center>;<large>;
|
||||
total general & admin expenses;<bold>;<soft>;;;;;
|
||||
total operating expenses;<bold>;rgb(128, 191, 255);;;;;
|
||||
Operating income before taxes;<bold>;rgb(0, 102, 204);;<white>;;<large>;
|
||||
Financial revenue & expenses;<bold>;;<italic>;;<center>;;<comment>
|
||||
Income before tax & extraordinary items;<bold>;rgb(0, 102, 204);;<white>;;<large>;
|
||||
Extraordinary items;<bold>;;<italic>;;<center>;;<comment>
|
||||
Net gain on sale of land;<bold>;rgb(0, 102, 204);<white>;;<center>;<large>;
|
||||
Extraordinary items after tax;<bold>;rgb(0, 102, 204);<white>;;<center>;<large>;
|
||||
Net Income (Profit);<bold>;<night>;;<white>;<center>;<large>;
|
||||
|
Binary file not shown.
@@ -1,21 +0,0 @@
|
||||
Accounts;Bold;Background;FontStyle;LetterColor;Align;Size;Comment
|
||||
Revenues;<bold>;;<italic>;;<center>;;<comment>
|
||||
Net sales revenues;<bold>;RGB(225,226,226);;;;;
|
||||
Cost of goods sold;<bold>;RGB(225,226,226);;;;;
|
||||
Manufacturing overhead;<bold>;;<italic>;;<center>;;<comment>
|
||||
Net mfr overhead;<bold>;RGB(225,226,226);;;;;
|
||||
Net costs of goods sold;<bold>;RGB(225,226,226);;;;;
|
||||
Gross profit;<bold>;RGB(193,216,47);;;<center>;<large>;
|
||||
Operating expenses;<bold>;;<italic>;;;;
|
||||
Total selling expenses;<bold>;RGB(225,226,226);;;;;
|
||||
General & administrative expenses;<bold>;;<italic>;;<center>;;<comment>
|
||||
Other general & admin expenses;<bold>;rgb(128, 191, 255);<white>;;<center>;<large>;
|
||||
total general & admin expenses;<bold>;<soft>;;;;;
|
||||
total operating expenses;<bold>;rgb(128, 191, 255);<white>;;;;
|
||||
Operating income before taxes;<bold>;RGB(193,216,47);;;;<large>;
|
||||
Financial revenue & expenses;<bold>;;<italic>;;<center>;;<comment>
|
||||
Income before tax & extraordinary items;<bold>;RGB(193,216,47);;;;<large>;
|
||||
Extraordinary items;<bold>;;<italic>;;<center>;;<comment>
|
||||
Net gain on sale of land;<bold>;RGB(193,216,47);;;<center>;<large>;
|
||||
Extraordinary items after tax;<bold>;RGB(193,216,47);;;<center>;<large>;
|
||||
Net Income (Profit);<bold>;<night>;;<white>;<center>;<large>;
|
||||
|
@@ -2,8 +2,8 @@
|
||||
set -o errexit
|
||||
|
||||
echo "Creating release for version: $VERSION"
|
||||
echo "Artifact name: ./build/${3}_${VERSION}.zip"
|
||||
$HOME/bin/ghr -t ${ghoauth} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} ${VERSION} "./build/${3}_${4}.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} "./dist/${3}_${4}.zip"
|
||||
|
||||
|
||||
# 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,160 +1,125 @@
|
||||
import qlik from 'qlik';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ApplyPreMask } from '../masking';
|
||||
import { addSeparators } from '../utilities';
|
||||
|
||||
function formatMeasurementValue (measurement, styling) {
|
||||
// TODO: measurement.name is a horrible propertyname, it's actually the column header
|
||||
const isColumnPercentageBased = measurement.parents.measurement.header.substring(0, 1) === '%';
|
||||
let formattedMeasurementValue = '';
|
||||
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;
|
||||
}
|
||||
|
||||
function getSemaphoreColors (measurement, semaphoreColors) {
|
||||
if (measurement < semaphoreColors.status.critical) {
|
||||
return semaphoreColors.statusColors.critical;
|
||||
}
|
||||
if (measurement < semaphoreColors.status.medium) {
|
||||
return semaphoreColors.statusColors.medium;
|
||||
}
|
||||
return semaphoreColors.statusColors.normal;
|
||||
}
|
||||
import Tooltip from '../tooltip/index.jsx';
|
||||
|
||||
class DataCell extends React.PureComponent {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.handleSelect = this.handleSelect.bind(this);
|
||||
}
|
||||
|
||||
handleSelect () {
|
||||
const { data: { meta: { dimensionCount } }, general: { allowFilteringByClick }, measurement, qlik } = this.props;
|
||||
const {
|
||||
data: {
|
||||
headers,
|
||||
meta: {
|
||||
dimensionCount,
|
||||
altState
|
||||
}
|
||||
},
|
||||
general: {
|
||||
allowFilteringByClick
|
||||
},
|
||||
measurement,
|
||||
component
|
||||
} = this.props;
|
||||
|
||||
const hasSecondDimension = dimensionCount > 1;
|
||||
if (!allowFilteringByClick) {
|
||||
return;
|
||||
}
|
||||
|
||||
qlik.backendApi.selectValues(0, [measurement.parents.dimension1.elementNumber], true);
|
||||
const app = qlik.currApp(component);
|
||||
app.field(headers.dimension1[0].name, altState)
|
||||
.select([measurement.parents.dimension1.elementNumber], false, false);
|
||||
|
||||
if (hasSecondDimension) {
|
||||
qlik.backendApi.selectValues(1, [measurement.parents.dimension2.elementNumber], true);
|
||||
app.field(headers.dimension2[0].name, altState)
|
||||
.select([measurement.parents.dimension2.elementNumber], false, false);
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { data, general, measurement, styleBuilder, styling } = this.props;
|
||||
const isColumnPercentageBased = measurement.name.substring(0, 1) === '%';
|
||||
let formattedMeasurementValue = formatMeasurementValue(measurement, styling);
|
||||
if (styleBuilder.hasComments()) {
|
||||
formattedMeasurementValue = '.';
|
||||
}
|
||||
let textAlignment = 'Right';
|
||||
const textAlignmentProp = styling.options.textAlignment;
|
||||
if (textAlignmentProp) {
|
||||
textAlignment = textAlignmentProp;
|
||||
}
|
||||
const {
|
||||
cellWidth,
|
||||
measurement,
|
||||
styleBuilder,
|
||||
styling
|
||||
} = this.props;
|
||||
|
||||
let textAlignment = styling.options.textAlignment || 'Right';
|
||||
|
||||
let cellStyle = {
|
||||
fontFamily: styling.options.fontFamily,
|
||||
...styleBuilder.getStyle(),
|
||||
paddingLeft: '4px',
|
||||
textAlign: textAlignment
|
||||
|
||||
paddingLeft: '5px',
|
||||
textAlign: textAlignment,
|
||||
minWidth: cellWidth,
|
||||
maxWidth: cellWidth
|
||||
};
|
||||
const { semaphoreColors } = styling;
|
||||
const isValidSemaphoreValue = !styleBuilder.hasComments() && !isNaN(measurement.value);
|
||||
const shouldHaveSemaphoreColors = semaphoreColors.fieldsToApplyTo.applyToAll || semaphoreColors.fieldsToApplyTo.specificFields.indexOf(measurement.parents.dimension1.header) !== -1;
|
||||
if (isValidSemaphoreValue && shouldHaveSemaphoreColors) {
|
||||
const { backgroundColor, color } = getSemaphoreColors(measurement, semaphoreColors);
|
||||
cellStyle = {
|
||||
backgroundColor,
|
||||
color,
|
||||
fontFamily: styling.options.fontFamily,
|
||||
fontSize: styleBuilder.getStyle().fontSize,
|
||||
paddingLeft: '4px',
|
||||
textAlign: textAlignment
|
||||
};
|
||||
|
||||
const isEmptyCell = measurement.displayValue === '';
|
||||
let formattedMeasurementValue;
|
||||
if (isEmptyCell || styleBuilder.hasComments()) {
|
||||
formattedMeasurementValue = '';
|
||||
cellStyle.cursor = 'default';
|
||||
} else {
|
||||
formattedMeasurementValue = formatMeasurementValue(measurement, styling);
|
||||
}
|
||||
|
||||
let cellClass = 'grid-cells';
|
||||
const shouldUseSmallCells = isColumnPercentageBased && data.headers.measurements.length > 1;
|
||||
if (shouldUseSmallCells) {
|
||||
cellClass = 'grid-cells-small';
|
||||
const { conditionalColoring } = styling;
|
||||
if (conditionalColoring.enabled) {
|
||||
const isValidConditionalColoringValue = !styleBuilder.hasComments() && !isNaN(measurement.value);
|
||||
const isSpecifiedRow =
|
||||
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 (
|
||||
<td
|
||||
className={`${cellClass}${general.cellSuffix}`}
|
||||
onClick={this.handleSelect}
|
||||
className="grid-cells"
|
||||
onClick={isEmptyCell ? null : this.handleSelect}
|
||||
style={cellStyle}
|
||||
>
|
||||
{formattedMeasurementValue}
|
||||
<Tooltip
|
||||
styling={styling}
|
||||
tooltipText={formattedMeasurementValue}
|
||||
>
|
||||
{formattedMeasurementValue}
|
||||
</Tooltip>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DataCell.propTypes = {
|
||||
cellWidth: PropTypes.string.isRequired,
|
||||
data: PropTypes.shape({
|
||||
headers: PropTypes.shape({
|
||||
dimension1: PropTypes.array.isRequired,
|
||||
measurements: PropTypes.array.isRequired
|
||||
}).isRequired,
|
||||
meta: PropTypes.shape({
|
||||
altState: PropTypes.string.isRequired,
|
||||
dimensionCount: PropTypes.number.isRequired
|
||||
}).isRequired
|
||||
}).isRequired,
|
||||
general: PropTypes.shape({
|
||||
cellSuffix: PropTypes.string.isRequired
|
||||
}).isRequired,
|
||||
general: PropTypes.shape({}).isRequired,
|
||||
measurement: PropTypes.shape({
|
||||
format: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
value: PropTypes.any
|
||||
}).isRequired,
|
||||
qlik: PropTypes.shape({
|
||||
backendApi: PropTypes.shape({
|
||||
selectValues: PropTypes.func.isRequired
|
||||
}).isRequired
|
||||
}).isRequired,
|
||||
component: PropTypes.shape({}).isRequired,
|
||||
styleBuilder: PropTypes.shape({
|
||||
hasComments: PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
@@ -164,3 +129,21 @@ DataCell.propTypes = {
|
||||
};
|
||||
|
||||
export default DataCell;
|
||||
|
||||
function formatMeasurementValue (measurement, styling) {
|
||||
if (isNaN(measurement.value)) {
|
||||
return styling.symbolForNulls;
|
||||
}
|
||||
|
||||
return measurement.displayValue;
|
||||
}
|
||||
|
||||
function getConditionalColor (measurement, conditionalColoring) {
|
||||
if (measurement.value < conditionalColoring.threshold.poor) {
|
||||
return conditionalColoring.colors.poor;
|
||||
}
|
||||
if (measurement.value < conditionalColoring.threshold.fair) {
|
||||
return conditionalColoring.colors.fair;
|
||||
}
|
||||
return conditionalColoring.colors.good;
|
||||
}
|
||||
|
||||
@@ -2,102 +2,117 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import StyleBuilder from '../style-builder';
|
||||
import DataCell from './data-cell.jsx';
|
||||
import HeaderPadding from './header-padding.jsx';
|
||||
import RowHeader from './row-header.jsx';
|
||||
import { injectSeparators } from '../utilities';
|
||||
|
||||
const DataTable = ({ data, general, qlik, renderData, styling }) => {
|
||||
const {
|
||||
headers: {
|
||||
dimension1,
|
||||
measurements
|
||||
},
|
||||
matrix
|
||||
} = data;
|
||||
class DataTable extends React.PureComponent {
|
||||
render () {
|
||||
const {
|
||||
cellWidth,
|
||||
columnSeparatorWidth,
|
||||
component,
|
||||
data,
|
||||
general,
|
||||
renderData,
|
||||
styling
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className="row-wrapper">
|
||||
<table>
|
||||
<tbody>
|
||||
{dimension1.map((dimensionEntry, dimensionIndex) => {
|
||||
const rowHeaderText = dimensionEntry.displayValue || '';
|
||||
if (rowHeaderText === '-') {
|
||||
return null;
|
||||
}
|
||||
const styleBuilder = new StyleBuilder(styling);
|
||||
if (styling.hasCustomFileStyle) {
|
||||
styleBuilder.parseCustomFileStyle(rowHeaderText);
|
||||
} else {
|
||||
styleBuilder.applyStandardAttributes(dimensionIndex);
|
||||
styleBuilder.applyCustomStyle({
|
||||
fontSize: `${14 + styling.options.fontSizeAdjustment}px`
|
||||
});
|
||||
}
|
||||
const rowStyle = {
|
||||
fontFamily: styling.options.fontFamily,
|
||||
width: '230px',
|
||||
...styleBuilder.getStyle()
|
||||
};
|
||||
const {
|
||||
headers: {
|
||||
dimension1,
|
||||
measurements
|
||||
},
|
||||
matrix
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<tr key={dimensionEntry.displayValue}>
|
||||
<RowHeader
|
||||
entry={dimensionEntry}
|
||||
qlik={qlik}
|
||||
rowStyle={rowStyle}
|
||||
styleBuilder={styleBuilder}
|
||||
styling={styling}
|
||||
/>
|
||||
{renderData && injectSeparators(
|
||||
matrix[dimensionIndex],
|
||||
styling.useSeparatorColumns,
|
||||
{ atEvery: measurements.length }
|
||||
).map((measurementData, index) => {
|
||||
if (measurementData.isSeparator) {
|
||||
const separatorStyle = {
|
||||
color: 'white',
|
||||
fontFamily: styling.options.fontFamily,
|
||||
fontSize: `${12 + styling.options.fontSizeAdjustment}px`
|
||||
};
|
||||
const separatorStyle = {
|
||||
minWidth: columnSeparatorWidth,
|
||||
maxWidth: columnSeparatorWidth
|
||||
};
|
||||
|
||||
return (
|
||||
<td
|
||||
className="empty"
|
||||
key={`${dimensionEntry.displayValue}-${index}-separator`}
|
||||
style={separatorStyle}
|
||||
>
|
||||
*
|
||||
</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}
|
||||
return (
|
||||
<div className="row-wrapper">
|
||||
<table>
|
||||
<tbody>
|
||||
{dimension1.map((dimensionEntry, dimensionIndex) => {
|
||||
const rowHeaderText = dimensionEntry.displayValue || '';
|
||||
if (rowHeaderText === '-') {
|
||||
return null;
|
||||
}
|
||||
const styleBuilder = new StyleBuilder(styling);
|
||||
if (styling.hasCustomFileStyle) {
|
||||
styleBuilder.parseCustomFileStyle(rowHeaderText);
|
||||
} else {
|
||||
styleBuilder.applyStandardAttributes(dimensionIndex);
|
||||
styleBuilder.applyCustomStyle({
|
||||
fontSize: `${14 + styling.options.fontSizeAdjustment}px`
|
||||
});
|
||||
}
|
||||
const rowStyle = {
|
||||
fontFamily: styling.options.fontFamily,
|
||||
width: '230px',
|
||||
...styleBuilder.getStyle()
|
||||
};
|
||||
|
||||
return (
|
||||
<tr key={dimensionEntry.displayValue}>
|
||||
{!renderData ?
|
||||
<RowHeader
|
||||
altState={data.meta.altState}
|
||||
component={component}
|
||||
entry={dimensionEntry}
|
||||
rowStyle={rowStyle}
|
||||
styleBuilder={styleBuilder}
|
||||
styling={styling}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
/> : null
|
||||
}
|
||||
{renderData && injectSeparators(
|
||||
matrix[dimensionIndex],
|
||||
columnSeparatorWidth,
|
||||
{ atEvery: measurements.length }
|
||||
).map((measurementData, index) => {
|
||||
if (measurementData.isSeparator) {
|
||||
return (
|
||||
<td
|
||||
className="empty"
|
||||
key={`${dimensionEntry.displayValue}-${index}-separator`}
|
||||
style={separatorStyle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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 = {
|
||||
renderData: true
|
||||
};
|
||||
|
||||
DataTable.propTypes = {
|
||||
cellWidth: PropTypes.string.isRequired,
|
||||
columnSeparatorWidth: PropTypes.string.isRequired,
|
||||
data: PropTypes.shape({
|
||||
headers: PropTypes.shape({
|
||||
dimension1: PropTypes.array.isRequired
|
||||
@@ -105,7 +120,7 @@ DataTable.propTypes = {
|
||||
matrix: PropTypes.arrayOf(PropTypes.array.isRequired).isRequired
|
||||
}).isRequired,
|
||||
general: PropTypes.shape({}).isRequired,
|
||||
qlik: PropTypes.shape({}).isRequired,
|
||||
component: PropTypes.shape({}).isRequired,
|
||||
renderData: PropTypes.bool,
|
||||
styling: PropTypes.shape({
|
||||
hasCustomFileStyle: PropTypes.bool.isRequired
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import qlik from 'qlik';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import HeaderPadding from './header-padding.jsx';
|
||||
import Tooltip from '../tooltip/index.jsx';
|
||||
|
||||
class RowHeader extends React.PureComponent {
|
||||
constructor (props) {
|
||||
@@ -10,23 +12,32 @@ class RowHeader extends React.PureComponent {
|
||||
}
|
||||
|
||||
handleSelect () {
|
||||
const { entry, qlik } = this.props;
|
||||
qlik.backendApi.selectValues(0, [entry.elementNumber], true);
|
||||
const { entry, altState, component } = this.props;
|
||||
const app = qlik.currApp(component);
|
||||
app.field(entry.name, altState).select([entry.elementNumber], false, false);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { entry, rowStyle, styleBuilder, styling } = this.props;
|
||||
const { entry, rowStyle, styleBuilder, styling, component } = this.props;
|
||||
const inEditState = component.inEditState();
|
||||
|
||||
return (
|
||||
<td
|
||||
className="fdim-cells"
|
||||
onClick={this.handleSelect}
|
||||
style={rowStyle}
|
||||
>
|
||||
<HeaderPadding
|
||||
styleBuilder={styleBuilder}
|
||||
<Tooltip
|
||||
isTooltipActive={!inEditState}
|
||||
styling={styling}
|
||||
/>
|
||||
{entry.displayValue}
|
||||
tooltipText={entry.displayValue}
|
||||
>
|
||||
<HeaderPadding
|
||||
styleBuilder={styleBuilder}
|
||||
styling={styling}
|
||||
/>
|
||||
{entry.displayValue}
|
||||
</Tooltip>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
@@ -34,13 +45,12 @@ class RowHeader extends React.PureComponent {
|
||||
|
||||
RowHeader.propTypes = {
|
||||
entry: PropTypes.shape({
|
||||
displayValue: PropTypes.string.isRequired
|
||||
}).isRequired,
|
||||
qlik: PropTypes.shape({
|
||||
backendApi: PropTypes.shape({
|
||||
selectValues: PropTypes.func.isRequired
|
||||
}).isRequired
|
||||
displayValue: PropTypes.string.isRequired,
|
||||
elementNumber: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired
|
||||
}).isRequired,
|
||||
altState: PropTypes.string.isRequired,
|
||||
component: PropTypes.shape({}).isRequired,
|
||||
rowStyle: PropTypes.shape({}).isRequired,
|
||||
styleBuilder: PropTypes.shape({}).isRequired,
|
||||
styling: PropTypes.shape({}).isRequired
|
||||
|
||||
67
src/dataset.js
Normal file
67
src/dataset.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import qlik from 'qlik';
|
||||
|
||||
function createCube (definition, app) {
|
||||
return new Promise(resolve => {
|
||||
app.createCube(definition, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
async function buildDataCube (originCubeDefinition, hasTwoDimensions, app) {
|
||||
const cubeDefinition = {
|
||||
...originCubeDefinition,
|
||||
qInitialDataFetch: [
|
||||
{
|
||||
qHeight: 1000,
|
||||
qWidth: 10
|
||||
}
|
||||
],
|
||||
qDimensions: [originCubeDefinition.qDimensions[0]],
|
||||
qMeasures: originCubeDefinition.qMeasures
|
||||
};
|
||||
if (hasTwoDimensions) {
|
||||
cubeDefinition.qDimensions.push(originCubeDefinition.qDimensions[1]);
|
||||
}
|
||||
const cube = await createCube(cubeDefinition, app);
|
||||
const cubeMatrix = cube.qHyperCube.qDataPages[0].qMatrix;
|
||||
app.destroySessionObject(cube.qInfo.qId);
|
||||
return cubeMatrix;
|
||||
}
|
||||
|
||||
export async function initializeDataCube (component, layout) {
|
||||
if (component.backendApi.isSnapshot) {
|
||||
return layout.snapshotData.dataCube;
|
||||
}
|
||||
|
||||
const app = qlik.currApp(component);
|
||||
const properties = (await component.backendApi.getProperties());
|
||||
|
||||
// If this is a master object, fetch the hyperCubeDef of the original object
|
||||
const hyperCubeDef = properties.qExtendsId
|
||||
? (await app.getObjectProperties(properties.qExtendsId)).properties.qHyperCubeDef
|
||||
: properties.qHyperCubeDef;
|
||||
hyperCubeDef.qStateName = layout.qStateName || "";
|
||||
|
||||
return buildDataCube(hyperCubeDef, layout.qHyperCube.qDimensionInfo.length === 2, app);
|
||||
}
|
||||
|
||||
export function initializeDesignList (component, layout) {
|
||||
if (component.backendApi.isSnapshot) {
|
||||
return layout.snapshotData.designList;
|
||||
}
|
||||
|
||||
if (!layout.stylingfield) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
const app = qlik.currApp(component);
|
||||
const stylingField = app.field(layout.stylingfield);
|
||||
const listener = function () {
|
||||
const data = stylingField.rows.map(row => row.qText);
|
||||
stylingField.OnData.unbind(listener);
|
||||
resolve(data);
|
||||
};
|
||||
stylingField.OnData.bind(listener);
|
||||
stylingField.getData();
|
||||
});
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
const colorLibrary = {
|
||||
type: 'items',
|
||||
label: 'Primary Colors Library',
|
||||
items: {
|
||||
ColLibClean: {
|
||||
ref: 'collibclean',
|
||||
translation: 'Clean',
|
||||
type: 'string',
|
||||
defaultValue: '#ffffff'
|
||||
},
|
||||
ColLibSoft: {
|
||||
ref: 'collibsoft',
|
||||
translation: 'Soft',
|
||||
type: 'string',
|
||||
defaultValue: '#efefef'
|
||||
},
|
||||
ColLibDark: {
|
||||
ref: 'collibdark',
|
||||
translation: 'Dark',
|
||||
type: 'string',
|
||||
defaultValue: '#c4c4c4'
|
||||
},
|
||||
ColLibNight: {
|
||||
ref: 'collibnight',
|
||||
translation: 'Night',
|
||||
type: 'string',
|
||||
defaultValue: '#808080'
|
||||
},
|
||||
ColLibRed: {
|
||||
ref: 'collibred',
|
||||
translation: 'Red',
|
||||
type: 'string',
|
||||
defaultValue: '#d58b94'
|
||||
},
|
||||
ColLibOrange: {
|
||||
ref: 'colliborange',
|
||||
translation: 'Orange',
|
||||
type: 'string',
|
||||
defaultValue: '#fd6600'
|
||||
},
|
||||
ColLibViolete: {
|
||||
ref: 'collibviolete',
|
||||
translation: 'Violete',
|
||||
type: 'string',
|
||||
defaultValue: '#ccc0ff'
|
||||
},
|
||||
ColLibBlue: {
|
||||
ref: 'collibblue',
|
||||
translation: 'Blue',
|
||||
type: 'string',
|
||||
defaultValue: '#4575b4'
|
||||
},
|
||||
ColLibGreen: {
|
||||
ref: 'collibgreen',
|
||||
translation: 'Green',
|
||||
type: 'string',
|
||||
defaultValue: '#7bb51c'
|
||||
},
|
||||
ColLibCustom: {
|
||||
ref: 'collibcustom',
|
||||
label: 'Custom',
|
||||
type: 'string',
|
||||
defaultValue: '#ffcccc'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default colorLibrary;
|
||||
@@ -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 = {
|
||||
type: 'items',
|
||||
label: 'Header Format',
|
||||
label: 'Header format',
|
||||
items: {
|
||||
Align: {
|
||||
ref: 'HeaderAlign',
|
||||
translation: 'Header Alignment',
|
||||
translation: 'Header alignment',
|
||||
type: 'number',
|
||||
component: 'buttongroup',
|
||||
options: [
|
||||
@@ -24,106 +24,30 @@ const header = {
|
||||
defaultValue: 2
|
||||
},
|
||||
headercolors: {
|
||||
component: 'color-picker',
|
||||
defaultValue: {
|
||||
index: 6,
|
||||
color: '#4477aa'
|
||||
},
|
||||
label: 'Background color',
|
||||
ref: 'HeaderColorSchema',
|
||||
type: 'string',
|
||||
component: 'dropdown',
|
||||
label: 'Background Header Color',
|
||||
options: [
|
||||
{
|
||||
value: 'Clean',
|
||||
label: 'Clean'
|
||||
},
|
||||
{
|
||||
value: 'Soft',
|
||||
label: 'Soft'
|
||||
},
|
||||
{
|
||||
value: 'Dark',
|
||||
label: 'Dark'
|
||||
},
|
||||
{
|
||||
value: 'Night',
|
||||
label: 'Night'
|
||||
},
|
||||
{
|
||||
value: 'Blue',
|
||||
label: 'Blue'
|
||||
},
|
||||
{
|
||||
value: 'Orange',
|
||||
label: 'Orange'
|
||||
},
|
||||
{
|
||||
value: 'Red',
|
||||
label: 'Red'
|
||||
},
|
||||
{
|
||||
value: 'Green',
|
||||
label: 'Green'
|
||||
},
|
||||
{
|
||||
value: 'Violete',
|
||||
label: 'Violete'
|
||||
},
|
||||
{
|
||||
value: 'Custom',
|
||||
label: 'Custom'
|
||||
}
|
||||
],
|
||||
defaultValue: 'Night'
|
||||
type: 'object',
|
||||
dualOutput: true
|
||||
},
|
||||
HeaderTextColor: {
|
||||
ref: 'HeaderTextColorSchema',
|
||||
type: 'string',
|
||||
component: 'dropdown',
|
||||
label: 'Text Header Color',
|
||||
options: [
|
||||
{
|
||||
value: 'Black',
|
||||
label: 'Black'
|
||||
},
|
||||
{
|
||||
value: 'DimGray',
|
||||
label: 'DimGray'
|
||||
},
|
||||
{
|
||||
value: 'ForestGreen',
|
||||
label: 'ForestGreen'
|
||||
},
|
||||
{
|
||||
value: 'Gainsboro',
|
||||
label: 'Gainsboro'
|
||||
},
|
||||
{
|
||||
value: 'Indigo',
|
||||
label: 'Indigo'
|
||||
},
|
||||
{
|
||||
value: 'Navy',
|
||||
label: 'Navy'
|
||||
},
|
||||
{
|
||||
value: 'Purple',
|
||||
label: 'Purple'
|
||||
},
|
||||
{
|
||||
value: 'WhiteSmoke',
|
||||
label: 'WhiteSmoke'
|
||||
},
|
||||
{
|
||||
value: 'White',
|
||||
label: 'White'
|
||||
},
|
||||
{
|
||||
value: 'YellowGreen',
|
||||
label: 'YellowGreen'
|
||||
}
|
||||
],
|
||||
defaultValue: 'WhiteSmoke'
|
||||
label: 'Text color',
|
||||
component: 'color-picker',
|
||||
defaultValue: {
|
||||
index: 1,
|
||||
color: '#ffffff'
|
||||
},
|
||||
type: 'object',
|
||||
dualOutput: true
|
||||
},
|
||||
HeaderFontSize: {
|
||||
ref: 'lettersizeheader',
|
||||
translation: 'Font Size',
|
||||
translation: 'Font size',
|
||||
type: 'number',
|
||||
component: 'buttongroup',
|
||||
options: [
|
||||
@@ -136,7 +60,7 @@ const header = {
|
||||
label: 'Medium'
|
||||
}
|
||||
],
|
||||
defaultValue: 2
|
||||
defaultValue: 1
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import pagination from './pagination';
|
||||
import header from './header';
|
||||
import formatted from './formatted';
|
||||
import conceptSemaphores from './concept-semaphores';
|
||||
import metricSemaphores from './metric-semaphores';
|
||||
import colorLibrary from './color-library';
|
||||
import pijamaColorLibrary from './pijama-color-library';
|
||||
import tableFormat from './table-format';
|
||||
import conditionalColoring from './conditional-coloring';
|
||||
|
||||
const definition = {
|
||||
component: 'accordion',
|
||||
@@ -20,20 +17,37 @@ const definition = {
|
||||
},
|
||||
uses: 'data'
|
||||
},
|
||||
sorting: {
|
||||
uses: 'sorting'
|
||||
},
|
||||
settings: {
|
||||
items: {
|
||||
ColorLibrary: colorLibrary,
|
||||
ConceptSemaphores: conceptSemaphores,
|
||||
Formatted: formatted,
|
||||
Formatted: tableFormat,
|
||||
Header: header,
|
||||
MetricSemaphores: metricSemaphores,
|
||||
Pagination: pagination,
|
||||
PijamaColorLibrary: pijamaColorLibrary
|
||||
ConditionalColoring: conditionalColoring,
|
||||
Pagination: pagination
|
||||
},
|
||||
uses: 'settings'
|
||||
},
|
||||
sorting: {
|
||||
uses: 'sorting'
|
||||
about: {
|
||||
component: 'items',
|
||||
label: 'About',
|
||||
items: {
|
||||
header: {
|
||||
label: 'P&L pivot',
|
||||
style: 'header',
|
||||
component: 'text'
|
||||
},
|
||||
paragraph1: {
|
||||
label: `P&L pivot is a Qlik Sense extension 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'
|
||||
|
||||
@@ -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: '0',
|
||||
show (data) {
|
||||
return data.allmetrics == false;
|
||||
}
|
||||
},
|
||||
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: 7,
|
||||
color: '#f93f17'
|
||||
}
|
||||
},
|
||||
ColorStatus1Text: {
|
||||
ref: 'colorstatus1text',
|
||||
label: 'Critic Color Text',
|
||||
type: 'object',
|
||||
component: 'color-picker',
|
||||
defaultValue: {
|
||||
index: 10,
|
||||
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: 8,
|
||||
color: '#ffcf02'
|
||||
}
|
||||
},
|
||||
ColorStatus2Text: {
|
||||
ref: 'colorstatus2text',
|
||||
label: 'Medium Color Text',
|
||||
type: 'object',
|
||||
component: 'color-picker',
|
||||
defaultValue: {
|
||||
index: 11,
|
||||
color: '#000000'
|
||||
}
|
||||
},
|
||||
ColorStatus3: {
|
||||
ref: 'colorstatus3',
|
||||
label: 'Success Color Fill',
|
||||
type: 'object',
|
||||
component: 'color-picker',
|
||||
defaultValue: {
|
||||
index: 9,
|
||||
color: '#276e27'
|
||||
}
|
||||
},
|
||||
ColorStatus3Text: {
|
||||
ref: 'colorstatus3text',
|
||||
label: 'Success Color Text',
|
||||
type: 'object',
|
||||
component: 'color-picker',
|
||||
defaultValue: {
|
||||
index: 10,
|
||||
color: '#ffffff'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default metricSemaphores;
|
||||
@@ -6,7 +6,7 @@ const pagination = {
|
||||
ref: 'maxloops',
|
||||
type: 'number',
|
||||
component: 'dropdown',
|
||||
label: 'Max Pagination Loops',
|
||||
label: 'Max pagination loops',
|
||||
options: [
|
||||
{
|
||||
value: 1,
|
||||
@@ -55,7 +55,7 @@ const pagination = {
|
||||
ref: 'errormessage',
|
||||
label: 'Default error message',
|
||||
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. Apply more filters to limit the amount of displayed data.'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
const pijamaColorLibrary = {
|
||||
type: 'items',
|
||||
label: 'Pijama Colors Library',
|
||||
items: {
|
||||
ColLibCleanP: {
|
||||
ref: 'collibcleanp',
|
||||
translation: 'Clean',
|
||||
type: 'string',
|
||||
defaultValue: '#ffffff'
|
||||
},
|
||||
ColLibSoftP: {
|
||||
ref: 'collibsoftp',
|
||||
translation: 'Soft',
|
||||
type: 'string',
|
||||
defaultValue: '#ffffff'
|
||||
},
|
||||
ColLibDarkP: {
|
||||
ref: 'collibdarkp',
|
||||
translation: 'Dark',
|
||||
type: 'string',
|
||||
defaultValue: '#efefef'
|
||||
},
|
||||
ColLibNightP: {
|
||||
ref: 'collibnightp',
|
||||
translation: 'Night',
|
||||
type: 'string',
|
||||
defaultValue: '#c4c4c4'
|
||||
},
|
||||
ColLibRedP: {
|
||||
ref: 'collibredp',
|
||||
translation: 'Red',
|
||||
type: 'string',
|
||||
defaultValue: '#ffcccc'
|
||||
},
|
||||
ColLibOrangeP: {
|
||||
ref: 'colliborangep',
|
||||
translation: 'Orange',
|
||||
type: 'string',
|
||||
defaultValue: '#ffcc66'
|
||||
},
|
||||
ColLibVioleteP: {
|
||||
ref: 'collibvioletep',
|
||||
translation: 'Violete',
|
||||
type: 'string',
|
||||
defaultValue: '#e6e6ff'
|
||||
},
|
||||
ColLibBlueP: {
|
||||
ref: 'collibbluep',
|
||||
translation: 'Blue',
|
||||
type: 'string',
|
||||
defaultValue: '#b3d9ff'
|
||||
},
|
||||
ColLibGreenP: {
|
||||
ref: 'collibgreenp',
|
||||
translation: 'Green',
|
||||
type: 'string',
|
||||
defaultValue: '#98fb98'
|
||||
},
|
||||
ColLibCustomP: {
|
||||
ref: 'collibcustomp',
|
||||
label: 'Custom',
|
||||
type: 'string',
|
||||
defaultValue: '#ffffff'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default pijamaColorLibrary;
|
||||
@@ -1,91 +1,95 @@
|
||||
const formatted = {
|
||||
const qlik = window.require('qlik');
|
||||
|
||||
// fixes case for when there are 3 dimensions, missies the case with 1 design dimension and 1 data dimension
|
||||
function hasDesignDimension (data) {
|
||||
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 = {
|
||||
type: 'items',
|
||||
label: 'Table Format',
|
||||
label: 'Table format',
|
||||
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: {
|
||||
ref: 'indentbool',
|
||||
type: 'boolean',
|
||||
label: 'Indent',
|
||||
defaultValue: true
|
||||
defaultValue: false,
|
||||
show: data => !hasDesignDimension(data)
|
||||
},
|
||||
SeparatorColumns: {
|
||||
ref: 'separatorcols',
|
||||
type: 'boolean',
|
||||
label: 'Separator Columns',
|
||||
label: 'Column separators',
|
||||
defaultValue: false
|
||||
},
|
||||
CustomFileBool: {
|
||||
ref: 'customfilebool',
|
||||
type: 'boolean',
|
||||
label: 'Include External File',
|
||||
defaultValue: false
|
||||
rowEvenBGColor: {
|
||||
component: 'color-picker',
|
||||
defaultValue: {
|
||||
color: '#fff',
|
||||
index: 1
|
||||
},
|
||||
label: 'Even row background color',
|
||||
ref: 'rowEvenBGColor',
|
||||
type: 'object',
|
||||
dualOutput: true,
|
||||
show: data => !hasDesignDimension(data)
|
||||
},
|
||||
CustomFile: {
|
||||
ref: 'customfile',
|
||||
label: 'Name of CSV file (; separated)',
|
||||
type: 'string',
|
||||
defaultValue: '',
|
||||
show (data) {
|
||||
return data.customfilebool;
|
||||
}
|
||||
},
|
||||
colors: {
|
||||
ref: 'ColorSchema',
|
||||
type: 'string',
|
||||
component: 'dropdown',
|
||||
label: 'BackGround Style',
|
||||
options: [
|
||||
{
|
||||
value: 'Clean',
|
||||
label: 'Clean'
|
||||
},
|
||||
{
|
||||
value: 'Soft',
|
||||
label: 'Soft'
|
||||
},
|
||||
{
|
||||
value: 'Dark',
|
||||
label: 'Dark'
|
||||
},
|
||||
{
|
||||
value: 'Night',
|
||||
label: 'Night'
|
||||
},
|
||||
{
|
||||
value: 'Blue',
|
||||
label: 'Blue'
|
||||
},
|
||||
{
|
||||
value: 'Orange',
|
||||
label: 'Orange'
|
||||
},
|
||||
{
|
||||
value: 'Red',
|
||||
label: 'Red'
|
||||
},
|
||||
{
|
||||
value: 'Green',
|
||||
label: 'Green'
|
||||
},
|
||||
{
|
||||
value: 'Violete',
|
||||
label: 'Violete'
|
||||
},
|
||||
{
|
||||
value: 'Custom',
|
||||
label: 'Custom'
|
||||
}
|
||||
],
|
||||
defaultValue: 'Clean',
|
||||
show (data) {
|
||||
return data.customfilebool == false;
|
||||
}
|
||||
rowOddBGColor: {
|
||||
component: 'color-picker',
|
||||
defaultValue: {
|
||||
color: '#b6d7ea',
|
||||
index: 4
|
||||
},
|
||||
label: 'Odd row background color',
|
||||
ref: 'rowOddBGColor',
|
||||
type: 'object',
|
||||
dualOutput: true,
|
||||
show: data => !hasDesignDimension(data)
|
||||
},
|
||||
BodyTextColor: {
|
||||
ref: 'BodyTextColorSchema',
|
||||
type: 'string',
|
||||
component: 'dropdown',
|
||||
label: 'Text Body Color',
|
||||
label: 'Text body color',
|
||||
options: [
|
||||
{
|
||||
value: 'Black',
|
||||
@@ -129,50 +133,48 @@ const formatted = {
|
||||
}
|
||||
],
|
||||
defaultValue: 'Black',
|
||||
show (data) {
|
||||
return data.customfilebool == false;
|
||||
}
|
||||
show: data => !hasDesignDimension(data)
|
||||
},
|
||||
FontFamily: {
|
||||
ref: 'FontFamily',
|
||||
type: 'string',
|
||||
component: 'dropdown',
|
||||
label: 'FontFamily',
|
||||
label: 'Font family',
|
||||
options: [
|
||||
{
|
||||
value: 'QlikView Sans',
|
||||
value: 'QlikView Sans, -apple-system, sans-serif',
|
||||
label: 'QlikView Sans'
|
||||
},
|
||||
{
|
||||
value: 'Arial',
|
||||
value: 'Arial, -apple-system, sans-serif',
|
||||
label: 'Arial'
|
||||
},
|
||||
{
|
||||
value: 'Calibri',
|
||||
value: 'Calibri, -apple-system, sans-serif',
|
||||
label: 'Calibri'
|
||||
},
|
||||
{
|
||||
value: 'Comic Sans MS',
|
||||
value: 'Comic Sans MS, -apple-system, sans-serif',
|
||||
label: 'Comic Sans MS'
|
||||
},
|
||||
{
|
||||
value: 'MS Sans Serif',
|
||||
value: 'MS Sans Serif, -apple-system, sans-serif',
|
||||
label: 'MS Sans Serif'
|
||||
},
|
||||
{
|
||||
value: 'Tahoma',
|
||||
value: 'Tahoma, -apple-system, sans-serif',
|
||||
label: 'Tahoma'
|
||||
},
|
||||
{
|
||||
value: 'Verdana',
|
||||
value: 'Verdana, -apple-system, sans-serif',
|
||||
label: 'Verdana'
|
||||
}
|
||||
],
|
||||
defaultValue: 'QlikView Sans'
|
||||
defaultValue: 'QlikView Sans, -apple-system, sans-serif'
|
||||
},
|
||||
DataFontSize: {
|
||||
ref: 'lettersize',
|
||||
translation: 'Font Size',
|
||||
translation: 'Font size',
|
||||
type: 'number',
|
||||
component: 'buttongroup',
|
||||
options: [
|
||||
@@ -189,7 +191,7 @@ const formatted = {
|
||||
},
|
||||
textAlignment: {
|
||||
ref: 'cellTextAlignment',
|
||||
label: 'Cell Text alignment',
|
||||
label: 'Cell text alignment',
|
||||
component: 'buttongroup',
|
||||
options: [
|
||||
{
|
||||
@@ -207,15 +209,33 @@ const formatted = {
|
||||
],
|
||||
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: {
|
||||
type: 'number',
|
||||
component: 'slider',
|
||||
label: 'Column Width',
|
||||
label: 'Column width',
|
||||
ref: 'columnwidthslider',
|
||||
min: 1,
|
||||
max: 3,
|
||||
step: 1,
|
||||
defaultValue: 2
|
||||
min: 20,
|
||||
max: 250,
|
||||
step: 10,
|
||||
defaultValue: 50,
|
||||
show: data => !data.fitchartwidth
|
||||
},
|
||||
SymbolForNulls: {
|
||||
ref: 'symbolfornulls',
|
||||
@@ -244,7 +264,7 @@ const formatted = {
|
||||
ref: 'filteroncellclick',
|
||||
type: 'boolean',
|
||||
component: 'switch',
|
||||
label: 'Filter data when cell clicked',
|
||||
label: 'Allow selection in cells',
|
||||
options: [
|
||||
{
|
||||
value: true,
|
||||
@@ -260,4 +280,4 @@ const formatted = {
|
||||
}
|
||||
};
|
||||
|
||||
export default formatted;
|
||||
export default tableFormat;
|
||||
@@ -1,19 +1,31 @@
|
||||
function removeAllTooltips (node) {
|
||||
const tooltips = node.querySelectorAll('.tooltip');
|
||||
[].forEach.call(tooltips, tooltip => {
|
||||
if (tooltip.parentNode) {
|
||||
tooltip.parentNode.removeChild(tooltip);
|
||||
function cleanupNodes (node) {
|
||||
const removables = node.querySelectorAll('.tooltip,input');
|
||||
[].forEach.call(removables, removeable => {
|
||||
if (removeable.parentNode) {
|
||||
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 subtitleHTML = `<p style="font-size:11pt">${subtitle}</p>`;
|
||||
const footnoteHTML = `<p style="font-size:11pt"><i>Note:</i>${footnote}</p>`;
|
||||
const dataTableClone = document.querySelector('.data-table').cloneNode(true);
|
||||
const footnoteHTML = `<p style="font-size:11pt">${footnote}</p>`;
|
||||
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 = `
|
||||
<html
|
||||
@@ -41,8 +53,23 @@ function buildTableHTML (title, subtitle, footnote) {
|
||||
<body>
|
||||
${titleHTML.length > 0 ? titleHTML : ''}
|
||||
${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 : ''}
|
||||
${dataTableClone.outerHTML}
|
||||
</body>
|
||||
</html>
|
||||
`.split('>.<')
|
||||
@@ -55,15 +82,15 @@ function buildTableHTML (title, subtitle, footnote) {
|
||||
|
||||
function downloadXLS (html) {
|
||||
const filename = 'analysis.xls';
|
||||
const blobObject = new Blob([html], { type: 'application/vnd.ms-excel' });
|
||||
|
||||
// IE/Edge
|
||||
if (window.navigator.msSaveOrOpenBlob) {
|
||||
const blobObject = new Blob([html]);
|
||||
return window.navigator.msSaveOrOpenBlob(blobObject, filename);
|
||||
}
|
||||
|
||||
const dataURI = generateDataURI(html);
|
||||
const link = window.document.createElement('a');
|
||||
link.href = dataURI;
|
||||
link.href = URL.createObjectURL(blobObject);
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
@@ -72,15 +99,8 @@ function downloadXLS (html) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function generateDataURI (html) {
|
||||
const dataType = 'data:application/vnd.ms-excel;base64,';
|
||||
const data = window.btoa(unescape(encodeURIComponent(html)));
|
||||
|
||||
return `${dataType}${data}`;
|
||||
}
|
||||
|
||||
export function exportXLS (title, subtitle, footnote) {
|
||||
export function exportXLS (containerElement, title, subtitle, footnote) {
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ class ExportButton extends React.PureComponent {
|
||||
}
|
||||
|
||||
handleExport () {
|
||||
const { excelExport, general } = this.props;
|
||||
const { component, excelExport, general } = this.props;
|
||||
const { title, subtitle, footnote } = general;
|
||||
if (excelExport) {
|
||||
exportXLS(title, subtitle, footnote);
|
||||
exportXLS(component.$element, title, subtitle, footnote);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ ExportButton.defaultProps = {
|
||||
};
|
||||
|
||||
ExportButton.propTypes = {
|
||||
component: PropTypes.shape({}).isRequired,
|
||||
excelExport: PropTypes.bool,
|
||||
general: PropTypes.shape({}).isRequired
|
||||
};
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
/* https://randomhaiku.com */
|
||||
|
||||
describe('behind the money', () => {
|
||||
describe('Canada and Panda work.', () => {
|
||||
it('Tiger starts blowing.', () => {
|
||||
expect(true).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,58 +1,69 @@
|
||||
import qlik from 'qlik';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { HEADER_FONT_SIZE } from '../initialize-transformed';
|
||||
import Tooltip from '../tooltip/index.jsx';
|
||||
|
||||
class ColumnHeader extends React.PureComponent {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.handleSelect = this.handleSelect.bind(this);
|
||||
}
|
||||
|
||||
handleSelect () {
|
||||
const { entry, qlik } = this.props;
|
||||
qlik.backendApi.selectValues(1, [entry.elementNumber], true);
|
||||
const { entry, altState, component } = this.props;
|
||||
const app = qlik.currApp(component);
|
||||
app.field(entry.name, altState).select([entry.elementNumber], false, false);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { baseCSS, cellSuffix, colSpan, entry, styling } = this.props;
|
||||
const { baseCSS, cellWidth, colSpan, component, entry, styling } = this.props;
|
||||
const inEditState = component.inEditState();
|
||||
const isMediumFontSize = styling.headerOptions.fontSizeAdjustment === HEADER_FONT_SIZE.MEDIUM;
|
||||
|
||||
const style = {
|
||||
...baseCSS,
|
||||
fontSize: `${14 + styling.headerOptions.fontSizeAdjustment} px`,
|
||||
height: '45px',
|
||||
verticalAlign: 'middle'
|
||||
fontSize: `${14 + styling.headerOptions.fontSizeAdjustment}px`,
|
||||
height: isMediumFontSize ? '43px' : '33px',
|
||||
verticalAlign: 'middle',
|
||||
minWidth: cellWidth,
|
||||
maxWidth: cellWidth
|
||||
};
|
||||
|
||||
return (
|
||||
<th
|
||||
className={`grid-cells2${cellSuffix}`}
|
||||
className="grid-cells"
|
||||
colSpan={colSpan}
|
||||
onClick={this.handleSelect}
|
||||
style={style}
|
||||
>
|
||||
{entry.displayValue}
|
||||
<Tooltip
|
||||
isTooltipActive={!inEditState}
|
||||
styling={styling}
|
||||
tooltipText={entry.displayValue}
|
||||
>
|
||||
{entry.displayValue}
|
||||
</Tooltip>
|
||||
</th>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ColumnHeader.defaultProps = {
|
||||
cellSuffix: '',
|
||||
colSpan: 1
|
||||
};
|
||||
|
||||
ColumnHeader.propTypes = {
|
||||
baseCSS: PropTypes.shape({}).isRequired,
|
||||
cellSuffix: PropTypes.string,
|
||||
cellWidth: PropTypes.string.isRequired,
|
||||
colSpan: PropTypes.number,
|
||||
entry: PropTypes.shape({
|
||||
displayValue: PropTypes.string.isRequired,
|
||||
elementNumber: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired
|
||||
}).isRequired,
|
||||
qlik: PropTypes.shape({
|
||||
backendApi: PropTypes.shape({
|
||||
selectValues: PropTypes.func.isRequired
|
||||
}).isRequired
|
||||
}).isRequired,
|
||||
altState: PropTypes.string.isRequired,
|
||||
component: PropTypes.shape({}).isRequired,
|
||||
styling: PropTypes.shape({
|
||||
headerOptions: PropTypes.shape({
|
||||
fontSizeAdjustment: PropTypes.number.isRequired
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ExportButton from '../export-button.jsx';
|
||||
import { HEADER_FONT_SIZE } from '../initialize-transformed';
|
||||
|
||||
const ExportColumnHeader = ({ baseCSS, general, title, allowExcelExport, hasSecondDimension, styling }) => {
|
||||
const ExportColumnHeader = ({ component, baseCSS, general, title, allowExcelExport, hasSecondDimension, styling }) => {
|
||||
const rowSpan = hasSecondDimension ? 2 : 1;
|
||||
const isMediumFontSize = styling.headerOptions.fontSizeAdjustment === HEADER_FONT_SIZE.MEDIUM;
|
||||
const style = {
|
||||
...baseCSS,
|
||||
cursor: 'default',
|
||||
fontSize: `${16 + styling.headerOptions.fontSizeAdjustment} px`,
|
||||
height: '80px',
|
||||
fontSize: `${16 + styling.headerOptions.fontSizeAdjustment}px`,
|
||||
height: isMediumFontSize ? '90px' : '70px',
|
||||
verticalAlign: 'middle',
|
||||
width: '230px'
|
||||
};
|
||||
@@ -20,6 +22,7 @@ const ExportColumnHeader = ({ baseCSS, general, title, allowExcelExport, hasSeco
|
||||
style={style}
|
||||
>
|
||||
<ExportButton
|
||||
component={component}
|
||||
excelExport={allowExcelExport}
|
||||
general={general}
|
||||
/>
|
||||
@@ -29,6 +32,7 @@ const ExportColumnHeader = ({ baseCSS, general, title, allowExcelExport, hasSeco
|
||||
};
|
||||
|
||||
ExportColumnHeader.propTypes = {
|
||||
component: PropTypes.shape({}).isRequired,
|
||||
allowExcelExport: PropTypes.bool.isRequired,
|
||||
baseCSS: PropTypes.shape({}).isRequired,
|
||||
general: PropTypes.shape({}).isRequired,
|
||||
|
||||
5
src/headers-table/index.componentModel.js
Normal file
5
src/headers-table/index.componentModel.js
Normal file
@@ -0,0 +1,5 @@
|
||||
function Model (component) {
|
||||
this.component = component;
|
||||
}
|
||||
|
||||
export default Model;
|
||||
@@ -5,134 +5,141 @@ import ColumnHeader from './column-header.jsx';
|
||||
import MeasurementColumnHeader from './measurement-column-header.jsx';
|
||||
import { injectSeparators } from '../utilities';
|
||||
|
||||
const HeadersTable = ({ data, general, qlik, styling }) => {
|
||||
const baseCSS = {
|
||||
backgroundColor: styling.headerOptions.colorSchema,
|
||||
color: styling.headerOptions.textColor,
|
||||
fontFamily: styling.options.fontFamily,
|
||||
textAlign: styling.headerOptions.alignment
|
||||
};
|
||||
class HeadersTable extends React.PureComponent {
|
||||
render () {
|
||||
const {
|
||||
cellWidth,
|
||||
columnSeparatorWidth,
|
||||
component,
|
||||
data,
|
||||
general,
|
||||
isKpi,
|
||||
styling
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
dimension1,
|
||||
dimension2,
|
||||
measurements
|
||||
} = data.headers;
|
||||
const baseCSS = {
|
||||
backgroundColor: styling.headerOptions.colorSchema,
|
||||
color: styling.headerOptions.textColor,
|
||||
fontFamily: styling.options.fontFamily,
|
||||
textAlign: styling.headerOptions.alignment
|
||||
};
|
||||
|
||||
const hasSecondDimension = dimension2.length > 0;
|
||||
const {
|
||||
dimension1,
|
||||
dimension2,
|
||||
measurements
|
||||
} = data.headers;
|
||||
|
||||
return (
|
||||
<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`
|
||||
};
|
||||
const hasSecondDimension = dimension2.length > 0;
|
||||
|
||||
return (
|
||||
<th
|
||||
className="empty"
|
||||
key={index}
|
||||
style={separatorStyle}
|
||||
>
|
||||
*
|
||||
</th>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ColumnHeader
|
||||
const separatorStyle = {
|
||||
minWidth: columnSeparatorWidth,
|
||||
maxWidth: columnSeparatorWidth
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="header-wrapper">
|
||||
<table className="header">
|
||||
<tbody>
|
||||
<tr>
|
||||
{isKpi ?
|
||||
<ExportColumnHeader
|
||||
allowExcelExport={general.allowExcelExport}
|
||||
baseCSS={baseCSS}
|
||||
cellSuffix={general.cellSuffix}
|
||||
colSpan={measurements.length}
|
||||
entry={entry}
|
||||
key={entry.displayValue}
|
||||
qlik={qlik}
|
||||
component={component}
|
||||
general={general}
|
||||
hasSecondDimension={hasSecondDimension}
|
||||
styling={styling}
|
||||
title={dimension1[0].name}
|
||||
/> : null
|
||||
}
|
||||
{!isKpi && !hasSecondDimension && measurements.map(measurementEntry => (
|
||||
<MeasurementColumnHeader
|
||||
baseCSS={baseCSS}
|
||||
cellWidth={cellWidth}
|
||||
hasSecondDimension={hasSecondDimension}
|
||||
key={`${measurementEntry.displayValue}-${measurementEntry.name}-${measurementEntry.index}`}
|
||||
measurement={measurementEntry}
|
||||
styling={styling}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
{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`
|
||||
};
|
||||
|
||||
))}
|
||||
{!isKpi && hasSecondDimension && injectSeparators(dimension2, columnSeparatorWidth).map((entry, index) => {
|
||||
if (entry.isSeparator) {
|
||||
return (
|
||||
<th
|
||||
className="empty"
|
||||
key={index}
|
||||
style={separatorStyle}
|
||||
>
|
||||
*
|
||||
</th>
|
||||
/>
|
||||
);
|
||||
}
|
||||
return measurements.map(measurementEntry => (
|
||||
<MeasurementColumnHeader
|
||||
return (
|
||||
<ColumnHeader
|
||||
altState={data.meta.altState}
|
||||
baseCSS={baseCSS}
|
||||
dimensionEntry={dimensionEntry}
|
||||
general={general}
|
||||
hasSecondDimension={hasSecondDimension}
|
||||
key={`${measurementEntry.displayValue}-${measurementEntry.name}-${dimensionEntry.name}`}
|
||||
measurement={measurementEntry}
|
||||
cellWidth={cellWidth}
|
||||
colSpan={measurements.length}
|
||||
component={component}
|
||||
entry={entry}
|
||||
key={entry.displayValue}
|
||||
styling={styling}
|
||||
/>
|
||||
));
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
{!isKpi && hasSecondDimension && (
|
||||
<tr>
|
||||
{injectSeparators(dimension2, columnSeparatorWidth).map((dimensionEntry, index) => {
|
||||
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 = {
|
||||
cellWidth: PropTypes.string.isRequired,
|
||||
columnSeparatorWidth: PropTypes.string.isRequired,
|
||||
data: PropTypes.shape({
|
||||
headers: PropTypes.shape({
|
||||
dimension1: PropTypes.array,
|
||||
dimension2: PropTypes.array,
|
||||
measurements: PropTypes.array
|
||||
}),
|
||||
meta: PropTypes.shape({
|
||||
altState: PropTypes.string.isRequired
|
||||
})
|
||||
}).isRequired,
|
||||
general: PropTypes.shape({}).isRequired,
|
||||
qlik: PropTypes.shape({
|
||||
backendApi: PropTypes.shape({
|
||||
selectValues: PropTypes.func.isRequired
|
||||
}).isRequired
|
||||
}).isRequired,
|
||||
component: PropTypes.shape({}).isRequired,
|
||||
styling: PropTypes.shape({
|
||||
headerOptions: PropTypes.shape({}),
|
||||
options: PropTypes.shape({})
|
||||
}).isRequired
|
||||
}).isRequired,
|
||||
isKpi: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default HeadersTable;
|
||||
|
||||
43
src/headers-table/index.spec.js
Normal file
43
src/headers-table/index.spec.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import merge from 'lodash.merge';
|
||||
import Model from './index.componentModel';
|
||||
import Component from './index.jsx';
|
||||
import { mountedComponent } from 'test-utilities';
|
||||
import sampleState from 'test-utilities/capex-sample-state';
|
||||
|
||||
|
||||
describe('<HeadersTable />', () => {
|
||||
const { data, general, styling } = sampleState;
|
||||
const defaultProps = {
|
||||
data,
|
||||
general,
|
||||
qlik: {
|
||||
backendApi: {
|
||||
selectValues: () => {}
|
||||
},
|
||||
inEditState: () => {}
|
||||
},
|
||||
styling
|
||||
};
|
||||
|
||||
function setup (otherProps = {}) {
|
||||
const props = merge(defaultProps, otherProps);
|
||||
|
||||
return mountedComponent(Model, Component, props);
|
||||
}
|
||||
|
||||
it('should render without exploding when 2 dimensions', () => {
|
||||
const model = setup();
|
||||
expect(model.component).toBeDefined();
|
||||
});
|
||||
|
||||
it('should render without exploding when 1 dimension', () => {
|
||||
const noSecondDimensionProps = {
|
||||
data: {
|
||||
...defaultProps.data.headers,
|
||||
dimension2: []
|
||||
}
|
||||
};
|
||||
const model = setup(noSecondDimensionProps);
|
||||
expect(model.component).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -1,55 +1,57 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { HEADER_FONT_SIZE } from '../initialize-transformed';
|
||||
import Tooltip from '../tooltip/index.jsx';
|
||||
|
||||
const MeasurementColumnHeader = ({ baseCSS, general, hasSecondDimension, measurement, styling }) => {
|
||||
const title = `${measurement.name} ${measurement.magnitudeLabelSuffix}`;
|
||||
const MeasurementColumnHeader = ({ baseCSS, cellWidth, hasSecondDimension, measurement, styling }) => {
|
||||
const title = `${measurement.name}`;
|
||||
const { fontSizeAdjustment } = styling.headerOptions;
|
||||
const isMediumFontSize = fontSizeAdjustment === HEADER_FONT_SIZE.MEDIUM;
|
||||
|
||||
const cellStyle = {
|
||||
...baseCSS,
|
||||
verticalAlign: 'middle',
|
||||
minWidth: cellWidth,
|
||||
maxWidth: cellWidth
|
||||
};
|
||||
|
||||
if (hasSecondDimension) {
|
||||
const isPercentageFormat = measurement.format.substring(measurement.format.length - 1) === '%';
|
||||
let baseFontSize = 14;
|
||||
let cellClass = 'grid-cells2';
|
||||
if (isPercentageFormat) {
|
||||
baseFontSize = 13;
|
||||
cellClass = 'grid-cells2-small';
|
||||
}
|
||||
const cellStyle = {
|
||||
...baseCSS,
|
||||
cursor: 'default',
|
||||
fontSize: `${baseFontSize + fontSizeAdjustment} px`,
|
||||
height: '25px',
|
||||
verticalAlign: 'middle'
|
||||
};
|
||||
cellStyle.fontSize = `${baseFontSize + fontSizeAdjustment}px`;
|
||||
cellStyle.height = isMediumFontSize ? '45px' : '35px';
|
||||
|
||||
return (
|
||||
<th
|
||||
className={`${cellClass}${general.cellSuffix}`}
|
||||
className="grid-cells"
|
||||
style={cellStyle}
|
||||
>
|
||||
<span className="wrapclass25">
|
||||
<Tooltip
|
||||
tooltipText={title}
|
||||
styling={styling}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</th>
|
||||
);
|
||||
}
|
||||
const isLong = (title.length > 11 && fontSizeAdjustment === 0) || (title.length > 12 && fontSizeAdjustment === -2);
|
||||
const suffixWrap = isLong ? '70' : 'empty';
|
||||
const style = {
|
||||
...baseCSS,
|
||||
cursor: 'default',
|
||||
fontSize: `${15 + fontSizeAdjustment} px`,
|
||||
height: '70px',
|
||||
verticalAlign: 'middle'
|
||||
};
|
||||
|
||||
cellStyle.fontSize = `${15 + fontSizeAdjustment}px`;
|
||||
cellStyle.height = isMediumFontSize ? '90px' : '70px';
|
||||
return (
|
||||
<th
|
||||
className={`grid-cells2 ${general.cellSuffix}`}
|
||||
style={style}
|
||||
className="grid-cells"
|
||||
style={cellStyle}
|
||||
>
|
||||
<span
|
||||
className={`wrapclass${suffixWrap}`}
|
||||
style={{ fontFamily: styling.headerOptions.fontFamily }}
|
||||
<Tooltip
|
||||
tooltipText={title}
|
||||
styling={styling}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</th>
|
||||
);
|
||||
};
|
||||
@@ -60,9 +62,7 @@ MeasurementColumnHeader.defaultProps = {
|
||||
|
||||
MeasurementColumnHeader.propTypes = {
|
||||
baseCSS: PropTypes.shape({}).isRequired,
|
||||
general: PropTypes.shape({
|
||||
cellSuffix: PropTypes.string.isRequired
|
||||
}).isRequired,
|
||||
cellWidth: PropTypes.string.isRequired,
|
||||
hasSecondDimension: PropTypes.bool,
|
||||
measurement: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired
|
||||
|
||||
57
src/index.js
57
src/index.js
@@ -1,5 +1,9 @@
|
||||
import paint from './paint.jsx';
|
||||
import definition from './definition';
|
||||
import { initializeDataCube, initializeDesignList } from './dataset';
|
||||
import initializeStore from './store';
|
||||
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
|
||||
@@ -10,27 +14,38 @@ export default {
|
||||
controller: [
|
||||
'$scope',
|
||||
'$timeout',
|
||||
function () { }
|
||||
function controller () {}
|
||||
],
|
||||
design: {
|
||||
dimensions: {
|
||||
max: 1,
|
||||
min: 0
|
||||
}
|
||||
},
|
||||
data: {
|
||||
dimensions: {
|
||||
max: 2,
|
||||
max: function (nMeasures) {
|
||||
return nMeasures < 9 ? 2 : 1;
|
||||
},
|
||||
min: 1,
|
||||
uses: 'dimensions'
|
||||
},
|
||||
measures: {
|
||||
max: 9,
|
||||
max: function (nDims) {
|
||||
return nDims < 2 ? 9 : 8;
|
||||
},
|
||||
min: 1,
|
||||
uses: 'measures'
|
||||
}
|
||||
},
|
||||
definition,
|
||||
initialProperties: {
|
||||
version: 1.0,
|
||||
qHyperCubeDef: {
|
||||
qDimensions: [],
|
||||
qInitialDataFetch: [
|
||||
{
|
||||
qHeight: 1000,
|
||||
qHeight: 1,
|
||||
qWidth: 10
|
||||
}
|
||||
],
|
||||
@@ -42,16 +57,34 @@ export default {
|
||||
exportData: true,
|
||||
snapshot: true
|
||||
},
|
||||
paint ($element, layout) {
|
||||
try {
|
||||
paint($element, layout, this);
|
||||
} catch (exception) {
|
||||
console.error(exception); // eslint-disable-line no-console
|
||||
throw exception;
|
||||
}
|
||||
paint: async function ($element, layout) {
|
||||
const dataCube = await initializeDataCube(this, layout);
|
||||
const designList = await initializeDesignList(this, layout);
|
||||
const state = await initializeStore({
|
||||
$element,
|
||||
component: this,
|
||||
dataCube,
|
||||
designList,
|
||||
layout
|
||||
});
|
||||
const editmodeClass = this.inAnalysisState() ? '' : 'edit-mode';
|
||||
const jsx = (
|
||||
<Root
|
||||
editmodeClass={editmodeClass}
|
||||
component={this}
|
||||
state={state}
|
||||
/>
|
||||
);
|
||||
|
||||
ReactDOM.render(jsx, $element[0]);
|
||||
},
|
||||
snapshot: {
|
||||
canTakeSnapshot: true
|
||||
},
|
||||
setSnapshotData: async function (snapshotLayout) {
|
||||
snapshotLayout.snapshotData.dataCube = await initializeDataCube(this, snapshotLayout);
|
||||
snapshotLayout.snapshotData.designList = await initializeDesignList(this, snapshotLayout);
|
||||
return snapshotLayout;
|
||||
},
|
||||
version: 1.0
|
||||
};
|
||||
|
||||
@@ -1,31 +1,9 @@
|
||||
import jQuery from 'jquery';
|
||||
import { distinctArray } from './utilities';
|
||||
|
||||
// TODO: rename colors
|
||||
function initializeColors ({ layout }) {
|
||||
return {
|
||||
vColLibBlue: layout.collibblue,
|
||||
vColLibBlueP: layout.collibbluep,
|
||||
vColLibClean: layout.collibclean,
|
||||
vColLibCleanP: layout.collibcleanp,
|
||||
vColLibCustom: layout.collibcustom,
|
||||
vColLibCustomP: layout.collibcustomp,
|
||||
vColLibDark: layout.collibdark,
|
||||
vColLibDarkP: layout.collibdarkp,
|
||||
vColLibGreen: layout.collibgreen,
|
||||
vColLibGreenP: layout.collibgreenp,
|
||||
vColLibNight: layout.collibnight,
|
||||
vColLibNightP: layout.collibnightp,
|
||||
vColLibOrange: layout.colliborange,
|
||||
vColLibOrangeP: layout.colliborangep,
|
||||
vColLibRed: layout.collibred,
|
||||
vColLibRedP: layout.collibredp,
|
||||
vColLibSoft: layout.collibsoft,
|
||||
vColLibSoftP: layout.collibsoftp,
|
||||
vColLibViolete: layout.collibviolete,
|
||||
vColLibVioleteP: layout.collibvioletep
|
||||
};
|
||||
}
|
||||
export const HEADER_FONT_SIZE = {
|
||||
SMALL: -1,
|
||||
MEDIUM: 1
|
||||
};
|
||||
|
||||
function getAlignment (option) {
|
||||
const alignmentOptions = {
|
||||
@@ -39,48 +17,17 @@ function getAlignment (option) {
|
||||
|
||||
function getFontSizeAdjustment (option) {
|
||||
const fontSizeAdjustmentOptions = {
|
||||
1: -1,
|
||||
2: 1,
|
||||
3: 2
|
||||
1: HEADER_FONT_SIZE.SMALL,
|
||||
2: HEADER_FONT_SIZE.MEDIUM
|
||||
};
|
||||
|
||||
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) {
|
||||
return information.map(measurement => {
|
||||
const format = getMeasurementFormat(measurement);
|
||||
const formatMagnitude = format.substr(format.length - 1).toLowerCase();
|
||||
const transformedMeasurement = {
|
||||
format,
|
||||
magnitudeLabelSuffix: getMagnitudeLabelSuffix(formatMagnitude),
|
||||
format: measurement.qNumFormat.qFmt || '#.##0',
|
||||
name: measurement.qFallbackTitle
|
||||
};
|
||||
|
||||
@@ -90,7 +37,7 @@ function generateMeasurements (information) {
|
||||
|
||||
function generateDimensionEntry (information, data) {
|
||||
return {
|
||||
displayValue: data.qText,
|
||||
displayValue: data.qText || data.qNum,
|
||||
elementNumber: data.qElemNumber,
|
||||
name: information.qFallbackTitle,
|
||||
value: data.qNum
|
||||
@@ -101,11 +48,6 @@ function generateMatrixCell ({ cell, dimension1Information, dimension2Informatio
|
||||
const matrixCell = {
|
||||
displayValue: cell.qText,
|
||||
format: measurementInformation.format,
|
||||
magnitude: measurementInformation.magnitudeLabelSuffix.substring(
|
||||
measurementInformation.magnitudeLabelSuffix.length - 2,
|
||||
measurementInformation.magnitudeLabelSuffix.length - 1
|
||||
),
|
||||
magnitudeLabelSuffix: measurementInformation.magnitudeLabelSuffix,
|
||||
name: measurementInformation.name,
|
||||
parents: {
|
||||
dimension1: {
|
||||
@@ -113,7 +55,8 @@ function generateMatrixCell ({ cell, dimension1Information, dimension2Informatio
|
||||
header: dimension1Information.qText
|
||||
},
|
||||
measurement: {
|
||||
header: measurementInformation.name
|
||||
header: measurementInformation.name,
|
||||
index: measurementInformation.index
|
||||
}
|
||||
},
|
||||
value: cell.qNum
|
||||
@@ -129,15 +72,16 @@ function generateMatrixCell ({ cell, dimension1Information, dimension2Informatio
|
||||
}
|
||||
|
||||
let lastRow = 0;
|
||||
function generateDataSet (component, dimensionsInformation, measurementsInformation) {
|
||||
const dimension1 = [];
|
||||
const dimension2 = [];
|
||||
function generateDataSet (
|
||||
component, dimensionsInformation, measurementsInformation, dataCube) {
|
||||
|
||||
const measurements = generateMeasurements(measurementsInformation);
|
||||
let dimension1 = [];
|
||||
let dimension2 = [];
|
||||
let matrix = [];
|
||||
|
||||
let previousDim1Entry;
|
||||
const hasSecondDimension = dimensionsInformation.length > 1;
|
||||
component.backendApi.eachDataRow((rowIndex, row) => {
|
||||
dataCube.forEach(row => {
|
||||
lastRow += 1;
|
||||
const dimension1Entry = generateDimensionEntry(dimensionsInformation[0], row[0]);
|
||||
dimension1.push(dimension1Entry);
|
||||
@@ -148,10 +92,11 @@ function generateDataSet (component, dimensionsInformation, measurementsInformat
|
||||
dimension2.push(dimension2Entry);
|
||||
firstDataCell = 2;
|
||||
}
|
||||
const matrixRow = row
|
||||
let matrixRow = row
|
||||
.slice(firstDataCell, row.length)
|
||||
.map((cell, cellIndex) => {
|
||||
const measurementInformation = measurements[cellIndex];
|
||||
measurementInformation.index = cellIndex;
|
||||
const dimension1Information = row[0]; // eslint-disable-line prefer-destructuring
|
||||
const dimension2Information = hasSecondDimension ? row[1] : null;
|
||||
const generatedCell = generateMatrixCell({
|
||||
@@ -164,37 +109,92 @@ function generateDataSet (component, dimensionsInformation, measurementsInformat
|
||||
return generatedCell;
|
||||
});
|
||||
|
||||
let appendToRowIndex = matrix.length;
|
||||
if (hasSecondDimension) {
|
||||
const currentDim1Entry = row[0].qText;
|
||||
const isSameDimension1AsPrevious = currentDim1Entry === previousDim1Entry;
|
||||
if (isSameDimension1AsPrevious) {
|
||||
const updatedRow = matrix[matrix.length - 1].concat(matrixRow);
|
||||
|
||||
matrix = [
|
||||
...matrix.slice(0, matrix.length - 1),
|
||||
updatedRow
|
||||
];
|
||||
} else {
|
||||
matrix[matrix.length] = matrixRow;
|
||||
// See if there already is a row for the current dim1
|
||||
for (let i = 0; i < matrix.length; i++) {
|
||||
if (matrix[i][0].parents.dimension1.header === matrixRow[0].parents.dimension1.header) {
|
||||
appendToRowIndex = i;
|
||||
matrixRow = matrix[i].concat(matrixRow);
|
||||
}
|
||||
}
|
||||
previousDim1Entry = currentDim1Entry;
|
||||
} else {
|
||||
matrix[matrix.length] = matrixRow;
|
||||
}
|
||||
matrix[appendToRowIndex] = matrixRow;
|
||||
});
|
||||
|
||||
// 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.elementNumber);
|
||||
});
|
||||
} else {
|
||||
appendMissingCells(row, newRow, 0, measurements, rowIndex);
|
||||
}
|
||||
|
||||
return newRow;
|
||||
});
|
||||
|
||||
return {
|
||||
dimension1: distinctArray(dimension1),
|
||||
dimension2: distinctArray(dimension2),
|
||||
dimension1: dimension1,
|
||||
dimension2: dimension2,
|
||||
matrix,
|
||||
measurements
|
||||
};
|
||||
}
|
||||
|
||||
async function initializeTransformed ({ $element, layout, component }) {
|
||||
const colors = initializeColors({ 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, dim1ElementNumber, dim2ElementNumber = -1) {
|
||||
|
||||
let index = sourceIndex;
|
||||
measurements.forEach((measurement, measureIndex) => {
|
||||
if (index < sourceRow.length
|
||||
&& (dim2ElementNumber === -1
|
||||
|| sourceRow[index].parents.dimension2.elementNumber === dim2ElementNumber)
|
||||
&& sourceRow[index].parents.measurement.header === measurement.name) {
|
||||
// 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: dim1ElementNumber },
|
||||
dimension2: { elementNumber: dim2ElementNumber },
|
||||
measurement: {
|
||||
header: measurement.name,
|
||||
index: measureIndex
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return index;
|
||||
}
|
||||
|
||||
function initializeTransformed ({ $element, component, dataCube, designList, layout }) {
|
||||
const dimensionsInformation = component.backendApi.getDimensionInfos();
|
||||
const measurementsInformation = component.backendApi.getMeasureInfos();
|
||||
const dimensionCount = layout.qHyperCube.qDimensionInfo.length;
|
||||
@@ -205,36 +205,39 @@ async function initializeTransformed ({ $element, layout, component }) {
|
||||
dimension2,
|
||||
measurements,
|
||||
matrix
|
||||
} = generateDataSet(component, dimensionsInformation, measurementsInformation);
|
||||
} = generateDataSet(component, dimensionsInformation, measurementsInformation, dataCube);
|
||||
|
||||
const customSchemaBasic = [];
|
||||
const customSchemaFull = [];
|
||||
let customHeadersCount = 0;
|
||||
|
||||
function readCustomSchema () {
|
||||
const url = `/Extensions/qlik-smart-pivot/${layout.customfile}`;
|
||||
if (designList && designList.length > 0) {
|
||||
const headers = designList[0].split(';');
|
||||
customHeadersCount = headers.length;
|
||||
for (let lineNumber = 0; lineNumber < designList.length; lineNumber += 1) {
|
||||
customSchemaFull[lineNumber] = new Array(headers.length);
|
||||
const data = designList[lineNumber].split(';');
|
||||
|
||||
return jQuery.get(url).then(response => {
|
||||
const allTextLines = response.split(/\r\n|\n/);
|
||||
const headers = allTextLines[0].split(';');
|
||||
customHeadersCount = headers.length;
|
||||
for (let lineNumber = 0; lineNumber < allTextLines.length; lineNumber += 1) {
|
||||
customSchemaFull[lineNumber] = new Array(headers.length);
|
||||
const data = allTextLines[lineNumber].split(';');
|
||||
|
||||
if (data.length === headers.length) {
|
||||
for (let headerIndex = 0; headerIndex < headers.length; headerIndex += 1) {
|
||||
customSchemaBasic[lineNumber] = data[0];
|
||||
customSchemaFull[lineNumber][headerIndex] = data[headerIndex];
|
||||
}
|
||||
if (data.length === headers.length) {
|
||||
for (let headerIndex = 0; headerIndex < headers.length; headerIndex += 1) {
|
||||
[customSchemaBasic[lineNumber]] = data;
|
||||
customSchemaFull[lineNumber][headerIndex] = data[headerIndex];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const hasCustomSchema = (layout.customfilebool && layout.customfile.length > 4);
|
||||
const schemaPromise = hasCustomSchema ? readCustomSchema() : Promise.resolve();
|
||||
await schemaPromise;
|
||||
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,
|
||||
// possibly with reselect for some presentational transforms (moving some of the presentational logic like formatting and such)
|
||||
@@ -247,81 +250,74 @@ async function initializeTransformed ({ $element, layout, component }) {
|
||||
},
|
||||
matrix, // 2d array of all rows/cells to render in body of datatable
|
||||
meta: {
|
||||
dimensionCount: dimensionsInformation.length
|
||||
dimensionCount: dimensionsInformation.length,
|
||||
altState: layout.qStateName || ""
|
||||
}
|
||||
},
|
||||
general: {
|
||||
allowExcelExport: layout.allowexportxls,
|
||||
allowFilteringByClick: layout.filteroncellclick,
|
||||
cellSuffix: getCellSuffix(layout.columnwidthslider), // TOOD: move to matrix cells or is it headers.measurements?
|
||||
cellWidth: cellWidth,
|
||||
errorMessage: layout.errormessage,
|
||||
footnote: layout.footnote,
|
||||
maxLoops,
|
||||
subtitle: layout.subtitle,
|
||||
title: layout.title
|
||||
title: layout.title,
|
||||
useColumnSeparator: layout.separatorcols && dimensionCount > 1
|
||||
},
|
||||
selection: {
|
||||
dimensionSelectionCounts: dimensionsInformation.map(dimensionInfo => dimensionInfo.qStateCounts.qSelected)
|
||||
},
|
||||
styling: {
|
||||
colors,
|
||||
customCSV: {
|
||||
basic: customSchemaBasic,
|
||||
count: customHeadersCount,
|
||||
full: customSchemaFull
|
||||
},
|
||||
hasCustomFileStyle: layout.customfilebool,
|
||||
hasCustomFileStyle: Boolean(designList),
|
||||
headerOptions: {
|
||||
alignment: getAlignment(layout.HeaderAlign),
|
||||
colorSchema: colors[`vColLib${layout.HeaderColorSchema}`],
|
||||
colorSchema: layout.HeaderColorSchema.color,
|
||||
fontSizeAdjustment: getFontSizeAdjustment(layout.lettersizeheader),
|
||||
textColor: layout.HeaderTextColorSchema
|
||||
textColor: layout.HeaderTextColorSchema.color
|
||||
},
|
||||
options: {
|
||||
backgroundColor: colors[`vColLib${layout.ColorSchema}`],
|
||||
backgroundColorOdd: colors[`vColLib${layout.ColorSchemaP}`],
|
||||
backgroundColor: layout.rowEvenBGColor,
|
||||
backgroundColorOdd: layout.rowOddBGColor,
|
||||
color: layout.BodyTextColorSchema,
|
||||
fontFamily: layout.FontFamily,
|
||||
fontSizeAdjustment: getFontSizeAdjustment(layout.lettersize),
|
||||
textAlignment: layout.cellTextAlignment
|
||||
},
|
||||
semaphoreColors: {
|
||||
fieldsToApplyTo: {
|
||||
applyToAll: layout.allsemaphores,
|
||||
specificFields: [
|
||||
layout.conceptsemaphore1,
|
||||
layout.conceptsemaphore2,
|
||||
layout.conceptsemaphore3,
|
||||
layout.conceptsemaphore4,
|
||||
layout.conceptsemaphore5,
|
||||
layout.conceptsemaphore6,
|
||||
layout.conceptsemaphore7,
|
||||
layout.conceptsemaphore9,
|
||||
layout.conceptsemaphore10
|
||||
]
|
||||
conditionalColoring: {
|
||||
enabled: layout.conditionalcoloring.enabled,
|
||||
colorAllRows: layout.conditionalcoloring.colorall,
|
||||
rows: layout.conditionalcoloring.rows.map(row => row.rowname),
|
||||
colorAllMeasures: typeof layout.conditionalcoloring.colorallmeasures === 'undefined'
|
||||
|| layout.conditionalcoloring.colorallmeasures,
|
||||
measures: !layout.conditionalcoloring.measures
|
||||
? [] : layout.conditionalcoloring.measures.split(',').map(index => Number(index)),
|
||||
threshold: {
|
||||
poor: layout.conditionalcoloring.threshold_poor,
|
||||
fair: layout.conditionalcoloring.threshold_fair
|
||||
},
|
||||
status: {
|
||||
critical: layout.metricstatus1,
|
||||
medium: layout.metricstatus2
|
||||
},
|
||||
statusColors: {
|
||||
critical: {
|
||||
backgroundColor: layout.colorstatus1.color,
|
||||
color: layout.colorstatus1text.color
|
||||
colors: {
|
||||
poor: {
|
||||
color: layout.conditionalcoloring.color_poor,
|
||||
textColor: layout.conditionalcoloring.textcolor_poor
|
||||
},
|
||||
medium: {
|
||||
backgroundColor: layout.colorstatus2.color,
|
||||
color: layout.colorstatus2text.color
|
||||
fair: {
|
||||
color: layout.conditionalcoloring.color_fair,
|
||||
textColor: layout.conditionalcoloring.textcolor_fair
|
||||
},
|
||||
normal: {
|
||||
backgroundColor: layout.colorstatus3.color,
|
||||
color: layout.colorstatus3text.color
|
||||
good: {
|
||||
color: layout.conditionalcoloring.color_good,
|
||||
textColor: layout.conditionalcoloring.textcolor_good
|
||||
}
|
||||
}
|
||||
},
|
||||
symbolForNulls: layout.symbolfornulls,
|
||||
usePadding: layout.indentbool,
|
||||
useSeparatorColumns: dimensionCount === 1 ? false : layout.separatorcols
|
||||
usePadding: layout.indentbool
|
||||
}
|
||||
};
|
||||
|
||||
@@ -339,7 +335,6 @@ async function initializeTransformed ({ $element, layout, component }) {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return transformedProperties;
|
||||
}
|
||||
|
||||
|
||||
363
src/main.less
363
src/main.less
@@ -1,229 +1,240 @@
|
||||
/* eslint-disable */
|
||||
.qv-object-qlik-smart-pivot {
|
||||
@TableBorder: 1px solid #d3d3d3;
|
||||
@KpiTableWidth: 230px;
|
||||
.edit-mode{
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.edit-mode {
|
||||
pointer-events: none;
|
||||
}
|
||||
._cell(@Width: 50px) {
|
||||
min-width: @Width!important;
|
||||
max-width: @Width!important;
|
||||
cursor: pointer;
|
||||
line-height: 1em!important;
|
||||
|
||||
.grid-cells {
|
||||
cursor: pointer;
|
||||
line-height: 1em !important;
|
||||
}
|
||||
|
||||
div.qv-object-content-container {
|
||||
z-index: 110;
|
||||
}
|
||||
div.qv-object-content-container {
|
||||
z-index: 110;
|
||||
}
|
||||
|
||||
.icon-xls {
|
||||
text-align: left;
|
||||
}
|
||||
.icon-xls {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
width: auto;
|
||||
border-left: @TableBorder;
|
||||
border-right: @TableBorder;
|
||||
border-top: @TableBorder;
|
||||
}
|
||||
table {
|
||||
border-collapse: separate;
|
||||
border-spacing: 1px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
td, th {
|
||||
border: 1px solid #ffffff;
|
||||
padding: 5px;
|
||||
border-collapse: collapse;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: middle;
|
||||
cursor: default;
|
||||
}
|
||||
tr {
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: 5px !important; // prevent overwriting from single object
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: middle;
|
||||
cursor: default;
|
||||
|
||||
.empty {
|
||||
width: 3%;
|
||||
background: #ffffff;
|
||||
min-width: 4px !important;
|
||||
max-width: 4px !important;
|
||||
}
|
||||
> div {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-family: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
th.main-kpi {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
border-bottom: @TableBorder;
|
||||
}
|
||||
.empty {
|
||||
background: #fff;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.numeric {
|
||||
text-align: right;
|
||||
}
|
||||
/*This is for wrap text in headers*/
|
||||
.wrapclass25 {
|
||||
width: 100%;
|
||||
height: 25px;
|
||||
white-space: pre-line;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
}
|
||||
th.main-kpi {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.wrapclass45 {
|
||||
width: 100%;
|
||||
height: 45px;
|
||||
white-space: pre-line;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
}
|
||||
.numeric {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.wrapclass70 {
|
||||
width: 100%;
|
||||
height: 70px;
|
||||
white-space: pre-line;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
line-height: 20px;
|
||||
}
|
||||
// First Column
|
||||
.fdim-cells {
|
||||
min-width: 230px !Important;
|
||||
max-width: 230px !Important;
|
||||
cursor: pointer;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.wrapclassEmpty {
|
||||
width: 100%;
|
||||
}
|
||||
/*******************/
|
||||
/* Medium column size*/
|
||||
/*******************/
|
||||
.grid-cells { ._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*/
|
||||
.fdim-cells {
|
||||
min-width: 230px !Important;
|
||||
max-width: 230px !Important;
|
||||
cursor: pointer;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.fdim-cells:hover {
|
||||
/*cursor: default;*/
|
||||
background-color: #808080 !important;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
tbody tr:hover {
|
||||
cursor: default;
|
||||
background-color: #808080 !important;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.grid-cells-header {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.grid-cells-title {
|
||||
min-width: 522px;
|
||||
}
|
||||
|
||||
.grid-cells:before {
|
||||
content: "\00a0";
|
||||
}
|
||||
tbody tr:hover td {
|
||||
cursor: default;
|
||||
background-color: #808080 !important;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.grid {
|
||||
height: 50px;
|
||||
width: 350px;
|
||||
height: 50px;
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
/*popups for headers*/
|
||||
.tooltip {
|
||||
position: fixed !important;
|
||||
color: RGB(70,70,70);
|
||||
background-color: RGB(245,239,207);
|
||||
text-align: center;
|
||||
border: groove;
|
||||
position: fixed !important;
|
||||
color: rgb(70, 70, 70);
|
||||
background-color: rgb(245, 239, 207);
|
||||
text-align: center;
|
||||
border: groove;
|
||||
}
|
||||
|
||||
/*end popups*/
|
||||
.row-wrapper {
|
||||
height: calc(~"100% - 97px");
|
||||
padding: 0;
|
||||
margin-top: 0;
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.kpi-table .fdim-cells, .data-table td {
|
||||
line-height: 1em!important;
|
||||
}
|
||||
|
||||
.data-table .fdim-cells {
|
||||
display: none;
|
||||
.kpi-table .fdim-cells,
|
||||
.data-table td {
|
||||
line-height: 1em !important;
|
||||
}
|
||||
|
||||
.kpi-table {
|
||||
width: @KpiTableWidth !important;
|
||||
overflow: hidden !important;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-right: 1px solid white;
|
||||
box-shadow: 4px 2px 8px #e1e1e1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
flex: none;
|
||||
width: @KpiTableWidth !important;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
.header-wrapper {
|
||||
flex: none;
|
||||
box-shadow: 4px 2px 8px #e1e1e1;
|
||||
}
|
||||
|
||||
.row-wrapper {
|
||||
height: calc(~"100% - 97px");
|
||||
overflow: scroll;
|
||||
position: absolute;
|
||||
padding: 0;
|
||||
margin-top: 0;
|
||||
margin: 0;
|
||||
margin-bottom: 8px;
|
||||
padding: 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.row-wrapper .fdim-cells {
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
height: 100%;
|
||||
width: calc(100% - 243px);
|
||||
position: absolute;
|
||||
margin-left: @KpiTableWidth + 13px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
margin-left: 13px;
|
||||
min-width: 0; /* This is to make flex size-filling work */
|
||||
|
||||
.header-wrapper {
|
||||
flex: none;
|
||||
overflow: scroll;
|
||||
width: 100%;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.row-wrapper {
|
||||
height: calc(~"100% - 97px");
|
||||
width: 100%;
|
||||
overflow: scroll;
|
||||
padding: 0;
|
||||
margin-top: 0;
|
||||
margin: 0;
|
||||
padding: 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
|
||||
.kpi-table .header-wrapper,
|
||||
.kpi-table .row-wrapper,
|
||||
.data-table .header-wrapper,
|
||||
.data-table .row-wrapper {
|
||||
-ms-overflow-style: none; // IE 10+
|
||||
-moz-overflow: -moz-scrollbars-none; // Firefox
|
||||
.data-table .header-wrapper {
|
||||
// stylelint-disable-next-line property-no-unknown
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none; // IE 10+
|
||||
-moz-overflow: -moz-scrollbars-none; // Firefox
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none; // Safari and Chrome
|
||||
display: none; // Safari and Chrome
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-wrapper {
|
||||
min-width: 25px;
|
||||
position: fixed;
|
||||
padding: 5px;
|
||||
padding-top: 8px;
|
||||
background-color: #404040;
|
||||
z-index: 100;
|
||||
pointer-events: none;
|
||||
border-radius: 5px;
|
||||
height: 30px;
|
||||
width: auto;
|
||||
opacity: 0.9;
|
||||
text-align: center;
|
||||
transform: translate(-50%, -110%);
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -10px;
|
||||
left: 50%;
|
||||
border-width: 10px 10px 0;
|
||||
border-style: solid;
|
||||
border-color: #404040 transparent;
|
||||
margin-left: -10px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
> p {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-enable */
|
||||
|
||||
131
src/masking.js
131
src/masking.js
@@ -1,131 +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 (mask, value) {
|
||||
if (!mask || isNaN(Number(value))) {
|
||||
return value; // return as it is.
|
||||
}
|
||||
|
||||
let isNegative, result, decimal, group, posLeadZero, posTrailZero, posSeparator,
|
||||
part, szSep, integer,
|
||||
|
||||
// find prefix/suffix
|
||||
len = mask.length,
|
||||
start = mask.search(/[0-9\-\+#]/),
|
||||
prefix = start > 0 ? mask.substring(0, start) : '',
|
||||
// reverse string: not an ideal method if there are surrogate pairs
|
||||
str = mask.split('').reverse()
|
||||
.join(''),
|
||||
end = str.search(/[0-9\-\+#]/),
|
||||
offset = len - end,
|
||||
substr = mask.substring(offset, offset + 1),
|
||||
indx = offset + ((substr === '.' || (substr === ',')) ? 1 : 0),
|
||||
suffix = end > 0 ? mask.substring(indx, len) : '';
|
||||
|
||||
// mask with prefix & suffix removed
|
||||
mask = mask.substring(start, indx);
|
||||
|
||||
// convert any string to number according to formation sign.
|
||||
value = mask.charAt(0) === '-' ? -value : Number(value);
|
||||
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);
|
||||
decimal = (result && result[result.length - 1]) || '.'; // treat the right most symbol as decimal
|
||||
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
|
||||
posTrailZero = mask[1] && mask[1].lastIndexOf('0'); // look for last zero in format
|
||||
part = value.split('.');
|
||||
// integer will get !part[1]
|
||||
if (!part[1] || (part[1] && part[1].length <= posTrailZero)) {
|
||||
value = (Number(value)).toFixed(posTrailZero + 1);
|
||||
}
|
||||
szSep = mask[0].split(group); // look for separator
|
||||
mask[0] = szSep.join(''); // join back without separator for counting the pos of any leading 0.
|
||||
|
||||
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.
|
||||
posSeparator = (szSep[1] && szSep[szSep.length - 1].length);
|
||||
if (posSeparator) {
|
||||
integer = value[0];
|
||||
str = '';
|
||||
offset = integer.length % posSeparator;
|
||||
len = integer.length;
|
||||
for (indx = 0; indx < len; indx++) {
|
||||
str += integer.charAt(indx); // ie6 only support charAt for sz.
|
||||
// -posSeparator so that won't trail separator on full length
|
||||
// jshint -W018
|
||||
if (!((indx - offset + 1) % posSeparator) && indx < 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,83 +0,0 @@
|
||||
import $ from 'jquery';
|
||||
import initializeStore from './store';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import HeadersTable from './headers-table/index.jsx';
|
||||
import DataTable from './data-table/index.jsx';
|
||||
import { LinkedScrollWrapper, LinkedScrollSection } from './linked-scroll';
|
||||
|
||||
export default async function paint ($element, layout, component) {
|
||||
const state = await initializeStore({
|
||||
$element,
|
||||
component,
|
||||
layout
|
||||
});
|
||||
const editmodeClass = component.inAnalysisState() ? '' : 'edit-mode';
|
||||
const jsx = (
|
||||
<LinkedScrollWrapper>
|
||||
<div className={`kpi-table ${editmodeClass}`}>
|
||||
<HeadersTable
|
||||
data={state.data}
|
||||
general={state.general}
|
||||
qlik={component}
|
||||
styling={state.styling}
|
||||
/>
|
||||
<LinkedScrollSection linkVertical>
|
||||
<DataTable
|
||||
data={state.data}
|
||||
general={state.general}
|
||||
qlik={component}
|
||||
renderData={false}
|
||||
styling={state.styling}
|
||||
/>
|
||||
</LinkedScrollSection>
|
||||
</div>
|
||||
<div className={`data-table ${editmodeClass}`}>
|
||||
<LinkedScrollSection linkHorizontal>
|
||||
<HeadersTable
|
||||
data={state.data}
|
||||
general={state.general}
|
||||
qlik={component}
|
||||
styling={state.styling}
|
||||
/>
|
||||
</LinkedScrollSection>
|
||||
<LinkedScrollSection
|
||||
linkHorizontal
|
||||
linkVertical
|
||||
>
|
||||
<DataTable
|
||||
data={state.data}
|
||||
general={state.general}
|
||||
qlik={component}
|
||||
styling={state.styling}
|
||||
/>
|
||||
</LinkedScrollSection>
|
||||
</div>
|
||||
</LinkedScrollWrapper>
|
||||
);
|
||||
|
||||
ReactDOM.render(jsx, $element[0]);
|
||||
|
||||
// TODO: fixing tooltips has a seperate issue, make sure to remove this as part of that issue
|
||||
$(`[tid="${layout.qInfo.qId}"] .header-wrapper th`).hover(function () {
|
||||
$(`[tid="${layout.qInfo.qId}"] .tooltip`).delay(500)
|
||||
.show(0);
|
||||
$(`[tid="${layout.qInfo.qId}"] .header-wrapper th`).children(`[tid="${layout.qInfo.qId}"] .tooltip`)
|
||||
.remove();
|
||||
|
||||
const element = $(this);
|
||||
const offset = element.offset();
|
||||
const toolTip = $('<div class="tooltip"></div>');
|
||||
|
||||
toolTip.css({
|
||||
left: offset.left,
|
||||
top: offset.top
|
||||
});
|
||||
|
||||
toolTip.text(element.text());
|
||||
$(`[tid="${layout.qInfo.qId}"] .header-wrapper th`).append(toolTip);
|
||||
}, () => {
|
||||
$(`[tid="${layout.qInfo.qId}"] .tooltip`).delay(0)
|
||||
.hide(0);
|
||||
});
|
||||
}
|
||||
5
src/root.componentModel.js
Normal file
5
src/root.componentModel.js
Normal file
@@ -0,0 +1,5 @@
|
||||
function Model (component) {
|
||||
this.component = component;
|
||||
}
|
||||
|
||||
export default Model;
|
||||
132
src/root.jsx
Normal file
132
src/root.jsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import HeadersTable from './headers-table/index.jsx';
|
||||
import DataTable from './data-table/index.jsx';
|
||||
import { LinkedScrollWrapper, LinkedScrollSection } from './linked-scroll';
|
||||
|
||||
class Root extends React.PureComponent {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.onDataTableRefSet = this.onDataTableRefSet.bind(this);
|
||||
this.renderedTableWidth = 0;
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
const tableWidth = this.dataTableRef.getBoundingClientRect().width;
|
||||
if (this.renderedTableWidth !== tableWidth) {
|
||||
this.forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
onDataTableRefSet (element) {
|
||||
this.dataTableRef = element;
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { editmodeClass, component, state } = this.props;
|
||||
const { data, general, styling } = state;
|
||||
|
||||
// Determine cell- and column separator width
|
||||
let cellWidth = '0px';
|
||||
let columnSeparatorWidth = '';
|
||||
if (this.dataTableRef) {
|
||||
const tableWidth = this.dataTableRef.getBoundingClientRect().width;
|
||||
this.renderedTableWidth = tableWidth;
|
||||
|
||||
if (general.cellWidth) {
|
||||
cellWidth = general.cellWidth;
|
||||
if (general.useColumnSeparator) {
|
||||
columnSeparatorWidth = '8px';
|
||||
}
|
||||
} else {
|
||||
const headerMarginRight = 8;
|
||||
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">
|
||||
<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 = {
|
||||
component: PropTypes.shape({}).isRequired,
|
||||
editmodeClass: PropTypes.string.isRequired,
|
||||
state: PropTypes.shape({
|
||||
data: PropTypes.object.isRequired,
|
||||
general: PropTypes.object.isRequired,
|
||||
styling: PropTypes.object.isRequired
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
export default Root;
|
||||
29
src/root.spec.js
Normal file
29
src/root.spec.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import merge from 'lodash.merge';
|
||||
import Model from './root.componentModel';
|
||||
import Component from './root.jsx';
|
||||
import { mountedComponent } from 'test-utilities';
|
||||
import sampleState from 'test-utilities/capex-sample-state';
|
||||
|
||||
describe('<Root />', () => {
|
||||
const state = sampleState;
|
||||
const defaultProps = {
|
||||
qlik: {
|
||||
backendApi: {
|
||||
selectValues: () => {}
|
||||
},
|
||||
inEditState: () => {}
|
||||
},
|
||||
state
|
||||
};
|
||||
|
||||
function setup (otherProps = {}) {
|
||||
const props = merge(defaultProps, otherProps);
|
||||
|
||||
return mountedComponent(Model, Component, props);
|
||||
}
|
||||
|
||||
it('should render without exploding', () => {
|
||||
const model = setup();
|
||||
expect(model.component).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,11 @@
|
||||
import initializeTransformed from './initialize-transformed';
|
||||
|
||||
async function initialize ({ $element, layout, component }) {
|
||||
async function initialize ({ $element, layout, component, dataCube, designList }) {
|
||||
const transformedProperties = await initializeTransformed({
|
||||
$element,
|
||||
component,
|
||||
dataCube,
|
||||
designList,
|
||||
layout
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
function StyleBuilder (styling) {
|
||||
const {
|
||||
colors,
|
||||
customCSV,
|
||||
options
|
||||
} = styling;
|
||||
@@ -12,9 +11,13 @@ function StyleBuilder (styling) {
|
||||
let hasCustomFileStyle = false;
|
||||
|
||||
function applyStandardAttributes (rowNumber) {
|
||||
const isEven = rowNumber % 2 === 0;
|
||||
style.backgroundColor = isEven ? options.backgroundColor : options.backgroundColorOdd;
|
||||
style.color = options.color;
|
||||
const hasBackgroundColor = options.backgroundColor && options.backgroundColor.color;
|
||||
const hasOddBackgroundColor = options.backgroundColorOdd && options.backgroundColorOdd.color;
|
||||
if (hasBackgroundColor && hasOddBackgroundColor) {
|
||||
const isEven = rowNumber % 2 === 0;
|
||||
style.backgroundColor = isEven ? options.backgroundColor.color : options.backgroundColorOdd.color;
|
||||
style.color = options.color;
|
||||
}
|
||||
style.fontSize = `${13 + options.fontSizeAdjustment}px`;
|
||||
}
|
||||
|
||||
@@ -30,16 +33,7 @@ function StyleBuilder (styling) {
|
||||
'<bold>': () => { style.fontWeight = 'bold'; },
|
||||
'<italic>': () => { style.fontStyle = 'italic'; },
|
||||
'<oblique>': () => { style.fontStyle = 'oblique'; },
|
||||
// background and comment color
|
||||
'<dark>': () => applyColor(colors.vColLibDark),
|
||||
'<night>': () => applyColor(colors.vColLibNight),
|
||||
'<soft>': () => applyColor(colors.vColLibSoft),
|
||||
'<red>': () => applyColor(colors.vColLibRed),
|
||||
'<orange>': () => applyColor(colors.vColLibOrange),
|
||||
'<violete>': () => applyColor(colors.vColLibViolete),
|
||||
'<blue>': () => applyColor(colors.vColLibBlue),
|
||||
'<green>': () => applyColor(colors.vColLibGreen),
|
||||
// font color TODO: this is a color just like the others, but it applies to text instead.. any way to make it less weird?
|
||||
// font color
|
||||
'<white>': () => { style.color = 'white'; },
|
||||
// font size
|
||||
'<large>': () => { style.fontSize = `${15 + options.fontSizeAdjustment}px`; },
|
||||
|
||||
65
src/tooltip/index.jsx
Normal file
65
src/tooltip/index.jsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const handleCalculateTooltipPosition = (event) => {
|
||||
const tooltip = document.querySelector('.tooltip-wrapper');
|
||||
if (!tooltip) {
|
||||
return;
|
||||
}
|
||||
tooltip.style.left = `${event.clientX}px`;
|
||||
tooltip.style.top = `${event.clientY}px`;
|
||||
};
|
||||
|
||||
class Tooltip extends React.PureComponent {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showTooltip: false
|
||||
};
|
||||
this.handleRenderTooltip = this.handleRenderTooltip.bind(this);
|
||||
}
|
||||
|
||||
handleRenderTooltip () {
|
||||
const { showTooltip } = this.state;
|
||||
this.setState({ showTooltip: !showTooltip });
|
||||
}
|
||||
|
||||
render () {
|
||||
const { children, styling, tooltipText } = this.props;
|
||||
const { showTooltip } = this.state;
|
||||
return (
|
||||
<div
|
||||
onMouseMove={handleCalculateTooltipPosition}
|
||||
onMouseOut={this.handleRenderTooltip}
|
||||
onMouseOver={this.handleRenderTooltip}
|
||||
style={{ fontFamily: styling.options.fontFamily }}
|
||||
>
|
||||
{children}
|
||||
|
||||
{showTooltip
|
||||
? (
|
||||
<div className="tooltip-wrapper">
|
||||
<p style={{ fontFamily: styling.options.fontFamily }}>
|
||||
{tooltipText}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Tooltip.propTypes = {
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node
|
||||
]).isRequired,
|
||||
styling: PropTypes.shape({
|
||||
options: PropTypes.shape({
|
||||
fontFamily: PropTypes.string.isRequired
|
||||
}).isRequired
|
||||
}).isRequired,
|
||||
tooltipText: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default Tooltip;
|
||||
@@ -9,19 +9,6 @@ export function distinctArray (array) {
|
||||
.map(entry => JSON.parse(entry));
|
||||
}
|
||||
|
||||
export function addSeparators (nStr, thousandsSep, decimalSep, numDecimals) {
|
||||
let x1;
|
||||
nStr = nStr.toFixed(numDecimals);
|
||||
const x = nStr.split('.');
|
||||
x1 = x[0];
|
||||
const x2 = x.length > 1 ? decimalSep + x[1] : '';
|
||||
const rgx = /(\d+)(\d{3})/;
|
||||
while (rgx.test(x1)) {
|
||||
x1 = x1.replace(rgx, `$1${thousandsSep}$2`);
|
||||
}
|
||||
return x1 + x2;
|
||||
}
|
||||
|
||||
export function Deferred () {
|
||||
this.promise = new Promise((resolve, reject) => {
|
||||
this.resolve = resolve;
|
||||
@@ -29,7 +16,7 @@ export function Deferred () {
|
||||
});
|
||||
}
|
||||
|
||||
export function injectSeparators (array, shouldHaveSeparator, suppliedOptions) {
|
||||
export function injectSeparators (array, columnSeparatorWidth, suppliedOptions) {
|
||||
const defaultOptions = {
|
||||
atEvery: 1,
|
||||
separator: { isSeparator: true }
|
||||
@@ -39,7 +26,7 @@ export function injectSeparators (array, shouldHaveSeparator, suppliedOptions) {
|
||||
...suppliedOptions
|
||||
};
|
||||
|
||||
if (!shouldHaveSeparator) {
|
||||
if (!columnSeparatorWidth) {
|
||||
return array;
|
||||
}
|
||||
return array.reduce((result, entry, index) => {
|
||||
|
||||
@@ -1,35 +1,129 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
rules: {
|
||||
"at-rule-no-unknown": true,
|
||||
"block-no-empty": true,
|
||||
"color-no-invalid-hex": true,
|
||||
"comment-no-empty": true,
|
||||
"declaration-block-no-duplicate-properties": [
|
||||
true,
|
||||
'rules': {
|
||||
'at-rule-empty-line-before': [
|
||||
'always',
|
||||
{
|
||||
ignore: ["consecutive-duplicates-with-different-values"]
|
||||
except: [
|
||||
'blockless-after-same-name-blockless',
|
||||
'first-nested'
|
||||
],
|
||||
ignore: ['after-comment']
|
||||
}
|
||||
],
|
||||
"declaration-block-no-shorthand-property-overrides": true,
|
||||
"font-family-no-duplicate-names": true,
|
||||
"font-family-no-missing-generic-family-keyword": true,
|
||||
"function-calc-no-unspaced-operator": true,
|
||||
"function-linear-gradient-no-nonstandard-direction": true,
|
||||
"keyframe-declaration-no-important": true,
|
||||
"media-feature-name-no-unknown": true,
|
||||
"no-descending-specificity": true,
|
||||
"no-duplicate-at-import-rules": true,
|
||||
"no-duplicate-selectors": true,
|
||||
"no-empty-source": true,
|
||||
"no-extra-semicolons": true,
|
||||
"no-invalid-double-slash-comments": true,
|
||||
"property-no-unknown": true,
|
||||
"selector-pseudo-class-no-unknown": true,
|
||||
"selector-pseudo-element-no-unknown": true,
|
||||
"selector-type-no-unknown": true,
|
||||
"string-no-newline": true,
|
||||
"unit-no-unknown": true
|
||||
'at-rule-name-case': 'lower',
|
||||
'at-rule-name-space-after': 'always-single-line',
|
||||
'at-rule-semicolon-newline-after': 'always',
|
||||
'block-closing-brace-empty-line-before': 'never',
|
||||
'block-closing-brace-newline-after': 'always',
|
||||
'block-closing-brace-newline-before': 'always-multi-line',
|
||||
'block-closing-brace-space-before': 'always-single-line',
|
||||
'block-opening-brace-newline-after': 'always-multi-line',
|
||||
'block-opening-brace-space-after': 'always-single-line',
|
||||
'block-opening-brace-space-before': 'always',
|
||||
'color-hex-case': 'lower',
|
||||
'color-hex-length': 'short',
|
||||
'color-named': 'never',
|
||||
'comment-empty-line-before': [
|
||||
'always',
|
||||
{
|
||||
except: ['first-nested'],
|
||||
ignore: ['stylelint-commands']
|
||||
}
|
||||
],
|
||||
'comment-whitespace-inside': 'always',
|
||||
'custom-property-empty-line-before': [
|
||||
'always',
|
||||
{
|
||||
except: [
|
||||
'after-custom-property',
|
||||
'first-nested'
|
||||
],
|
||||
ignore: [
|
||||
'after-comment',
|
||||
'inside-single-line-block'
|
||||
]
|
||||
}
|
||||
],
|
||||
'declaration-bang-space-after': 'never',
|
||||
'declaration-bang-space-before': 'always',
|
||||
'declaration-block-semicolon-newline-after': 'always-multi-line',
|
||||
'declaration-block-semicolon-space-after': 'always-single-line',
|
||||
'declaration-block-semicolon-space-before': 'never',
|
||||
'declaration-block-single-line-max-declarations': 1,
|
||||
'declaration-block-trailing-semicolon': 'always',
|
||||
'declaration-colon-newline-after': 'always-multi-line',
|
||||
'declaration-colon-space-after': 'always-single-line',
|
||||
'declaration-colon-space-before': 'never',
|
||||
'declaration-empty-line-before': [
|
||||
'always',
|
||||
{
|
||||
except: [
|
||||
'after-declaration',
|
||||
'first-nested'
|
||||
],
|
||||
ignore: [
|
||||
'after-comment',
|
||||
'inside-single-line-block'
|
||||
]
|
||||
}
|
||||
],
|
||||
'declaration-no-important': [
|
||||
true,
|
||||
{
|
||||
severity: 'warning'
|
||||
}
|
||||
],
|
||||
'function-comma-newline-after': 'always-multi-line',
|
||||
'function-comma-space-after': 'always-single-line',
|
||||
'function-comma-space-before': 'never',
|
||||
'function-max-empty-lines': 0,
|
||||
'function-name-case': 'lower',
|
||||
'function-parentheses-newline-inside': 'always-multi-line',
|
||||
'function-parentheses-space-inside': 'never-single-line',
|
||||
'function-whitespace-after': 'always',
|
||||
'indentation': 2,
|
||||
'length-zero-no-unit': true,
|
||||
'max-empty-lines': 1,
|
||||
'max-nesting-depth': 5,
|
||||
'media-feature-colon-space-after': 'always',
|
||||
'media-feature-colon-space-before': 'never',
|
||||
'media-feature-name-case': 'lower',
|
||||
'media-feature-parentheses-space-inside': 'never',
|
||||
'media-feature-range-operator-space-after': 'always',
|
||||
'media-feature-range-operator-space-before': 'always',
|
||||
'media-query-list-comma-newline-after': 'always-multi-line',
|
||||
'media-query-list-comma-space-after': 'always-single-line',
|
||||
'media-query-list-comma-space-before': 'never',
|
||||
'no-extra-semicolons': true,
|
||||
'no-missing-end-of-source-newline': true,
|
||||
'number-leading-zero': 'always',
|
||||
'number-no-trailing-zeros': true,
|
||||
'property-case': 'lower',
|
||||
'rule-empty-line-before': [
|
||||
'always-multi-line',
|
||||
{
|
||||
except: ['first-nested'],
|
||||
ignore: ['after-comment']
|
||||
}
|
||||
],
|
||||
'selector-attribute-brackets-space-inside': 'never',
|
||||
'selector-attribute-operator-space-after': 'never',
|
||||
'selector-attribute-operator-space-before': 'never',
|
||||
'selector-combinator-space-after': 'always',
|
||||
'selector-combinator-space-before': 'always',
|
||||
'selector-descendant-combinator-no-non-space': true,
|
||||
'selector-list-comma-newline-after': 'always',
|
||||
'selector-list-comma-space-before': 'never',
|
||||
'selector-max-empty-lines': 0,
|
||||
'selector-pseudo-class-case': 'lower',
|
||||
'selector-pseudo-class-parentheses-space-inside': 'never',
|
||||
'selector-pseudo-element-case': 'lower',
|
||||
'selector-pseudo-element-colon-notation': 'double',
|
||||
'selector-type-case': 'lower',
|
||||
'unit-case': 'lower',
|
||||
'value-list-comma-newline-after': 'always-multi-line',
|
||||
'value-list-comma-space-after': 'always-single-line',
|
||||
'value-list-comma-space-before': 'never',
|
||||
'value-list-max-empty-lines': 0
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
const CopyWebpackPlugin = require('copy-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 = {
|
||||
devtool: 'source-map',
|
||||
@@ -13,13 +16,15 @@ const config = {
|
||||
commonjs: 'jquery',
|
||||
commonjs2: 'jquery',
|
||||
root: '_'
|
||||
},
|
||||
qlik: {
|
||||
amd: 'qlik',
|
||||
commonjs: 'qlik',
|
||||
commonjs2: 'qlik',
|
||||
root: '_'
|
||||
}
|
||||
},
|
||||
mode: settings.mode,
|
||||
// TODO: breaks core-js for some reason
|
||||
// resolve: {
|
||||
// extensions: ['js', 'jsx']
|
||||
// },
|
||||
mode: MODE,
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
@@ -59,23 +64,14 @@ const config = {
|
||||
]
|
||||
},
|
||||
output: {
|
||||
filename: `${settings.name}.js`,
|
||||
filename: `${packageJSON.name}.js`,
|
||||
libraryTarget: 'amd',
|
||||
path: settings.buildDestination
|
||||
path: DIST
|
||||
},
|
||||
plugins: [
|
||||
new CopyWebpackPlugin([
|
||||
`assets/${settings.name}.qext`,
|
||||
`assets/${settings.name}.png`,
|
||||
'assets/wbfolder.wbl',
|
||||
'resources/Excel.png',
|
||||
|
||||
// TODO: remove entries below this line
|
||||
'resources/Accounts.csv',
|
||||
'resources/Accounts2.csv',
|
||||
'resources/QlikLook.csv'
|
||||
], {}),
|
||||
new StyleLintPlugin()
|
||||
new StyleLintPlugin({
|
||||
files: '**/*.less'
|
||||
})
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user