Compare commits
182 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48f90f20b2 | ||
|
|
b1ffb3a9d1 | ||
|
|
090a47cffd | ||
|
|
43fa4cd380 | ||
|
|
233f2a5c7c | ||
|
|
ca6893296a | ||
|
|
f13312c288 | ||
|
|
6fc73c4cc8 | ||
|
|
9424e847c9 | ||
|
|
b86a3ae9c6 | ||
|
|
ed50a8f3d3 | ||
|
|
cca315efbb | ||
|
|
c888dd9fe4 | ||
|
|
467948a46c | ||
|
|
315191ccfa | ||
|
|
80a9fdf2a9 | ||
|
|
092b89d69f | ||
|
|
f934fa929d | ||
|
|
acf6eb7b26 | ||
|
|
d6ae86220e | ||
|
|
3397bd419d | ||
|
|
2dd11ad24c | ||
|
|
1099be87fa | ||
|
|
13c5b9613d | ||
|
|
32890ba3db | ||
|
|
8f7465dd8d | ||
|
|
be710cb17b | ||
|
|
2729321f40 | ||
|
|
2710d4629f | ||
|
|
8c093a4692 | ||
|
|
ec822b843b | ||
|
|
5dc8bb49a4 | ||
|
|
0d98553a71 | ||
|
|
6513a294b3 | ||
|
|
79e753c2b2 | ||
|
|
468598540f | ||
|
|
045d0db215 | ||
|
|
73011d0388 | ||
|
|
41cf77e8d2 | ||
|
|
1355381346 | ||
|
|
730f35a83c | ||
|
|
71bf25e8fb | ||
|
|
9b4fe54239 | ||
|
|
e59c594215 | ||
|
|
97a54e6f5a | ||
|
|
eac9fd2a5f | ||
|
|
96f09f9323 | ||
|
|
2ab340f3f1 | ||
|
|
58d0f542eb | ||
|
|
8ba826a0ea | ||
|
|
d2446395e2 | ||
|
|
f17a7b7714 | ||
|
|
8aa86275f0 | ||
|
|
3ebc2b9e29 | ||
|
|
ce81549011 | ||
|
|
e64af66dab | ||
|
|
fd9e645fc1 | ||
|
|
ca1b1a9b53 | ||
|
|
a65f843008 | ||
|
|
be9570e0aa | ||
|
|
41d3a7c9af | ||
|
|
1e0a7c1204 | ||
|
|
17b5df296c | ||
|
|
64b778b1af | ||
|
|
be6718ccf4 | ||
|
|
28e6237a43 | ||
|
|
4fab3b5933 | ||
|
|
79339a8963 | ||
|
|
a6faeeb656 | ||
|
|
c22b7f5c6b | ||
|
|
80f97602e4 | ||
|
|
f745656b4c | ||
|
|
f7e780b92e | ||
|
|
d7a76c7db9 | ||
|
|
16c380e1c6 | ||
|
|
072a3b80c4 | ||
|
|
5572b7ce67 | ||
|
|
71cf92c217 | ||
|
|
e26d5fded8 | ||
|
|
20282b0b99 | ||
|
|
5f18321ccf | ||
|
|
d14f5951ac | ||
|
|
5872ee7b58 | ||
|
|
1436463f59 | ||
|
|
729a31920d | ||
|
|
6d305b21b2 | ||
|
|
0b210e0d35 | ||
|
|
c8afb83130 | ||
|
|
49981f6ae3 | ||
|
|
b3b17e8d0c | ||
|
|
5d45f57e00 | ||
|
|
2f59d97cf3 | ||
|
|
b8d6b0a53e | ||
|
|
6cc82e7b38 | ||
|
|
cc7e3e62ed | ||
|
|
26de3b63ed | ||
|
|
8eafeffcec | ||
|
|
ef7926dd13 | ||
|
|
af307fd24b | ||
|
|
98401db922 | ||
|
|
75771e4815 | ||
|
|
a12205c840 | ||
|
|
ebfee69c7b | ||
|
|
05c25c72cb | ||
|
|
d2a24dd256 | ||
|
|
a6a6ef24b5 | ||
|
|
8374ecce85 | ||
|
|
0135d4fc64 | ||
|
|
47d7f33cb9 | ||
|
|
4c2e483592 | ||
|
|
801c7c862e | ||
|
|
f3bda74202 | ||
|
|
3ec5d13ae6 | ||
|
|
2133bb44ef | ||
|
|
e447666982 | ||
|
|
7038140088 | ||
|
|
b5d9633496 | ||
|
|
a518432db4 | ||
|
|
f1875702be | ||
|
|
b1a9130663 | ||
|
|
dfb30285ab | ||
|
|
c8cd78ae5f | ||
|
|
a5bc3ecd1b | ||
|
|
bec68e7cd4 | ||
|
|
02bba4ad5a | ||
|
|
57c4b12b24 | ||
|
|
ca33540fd6 | ||
|
|
cac3fabb2f | ||
|
|
c57b0edea8 | ||
|
|
3862bd294c | ||
|
|
0c18523891 | ||
|
|
629821bd6b | ||
|
|
08c5cf8104 | ||
|
|
d3c39bea75 | ||
|
|
141be3f962 | ||
|
|
433a725f33 | ||
|
|
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 | ||
|
|
a25b2c40c0 | ||
|
|
46d6520273 | ||
|
|
19286f6c56 |
@@ -3,7 +3,7 @@ version: 2
|
||||
defaults: &defaults
|
||||
working_directory: ~/qlik-smart-pivot
|
||||
docker:
|
||||
- image: circleci/node:stretch
|
||||
- image: circleci/node:16.13.0
|
||||
environment:
|
||||
GITHUB_ORG: "qlik-oss"
|
||||
GITHUB_REPO: "PLSmartPivot"
|
||||
@@ -12,23 +12,12 @@ defaults: &defaults
|
||||
jobs:
|
||||
test:
|
||||
docker:
|
||||
- image: circleci/node:stretch-browsers
|
||||
- image: circleci/node:16.13.0-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Install dependencies
|
||||
command: npm install
|
||||
- run:
|
||||
name: BlackDuck scan
|
||||
command: curl -s https://blackducksoftware.github.io/hub-detect/hub-detect.sh | bash -s -- \
|
||||
--blackduck.url="https://qliktech.blackducksoftware.com" \
|
||||
--blackduck.trust.cert=true \
|
||||
--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
|
||||
@@ -46,6 +35,10 @@ jobs:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Setup environment
|
||||
command: |
|
||||
sudo chmod +x ./.circleci/upgrade-node.sh
|
||||
- attach_workspace:
|
||||
at: ~/qlik-smart-pivot
|
||||
- run:
|
||||
@@ -56,16 +49,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:
|
||||
|
||||
17
.circleci/upgrade-node.sh
Normal file
17
.circleci/upgrade-node.sh
Normal file
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
NVM_DIR="/opt/circleci/.nvm"
|
||||
NODE_VERSION="v16"
|
||||
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||
|
||||
echo "Installing Node $NODE_VERSION"
|
||||
nvm install $NODE_VERSION
|
||||
nvm alias default $NODE_VERSION
|
||||
|
||||
# Each step uses the same `$BASH_ENV`, so need to modify it
|
||||
echo 'export NVM_DIR="/opt/circleci/.nvm"' >> $BASH_ENV
|
||||
echo "[ -s \"$NVM_DIR/nvm.sh\" ] && . \"$NVM_DIR/nvm.sh\"" >> $BASH_ENV
|
||||
75
.eslintrc.js
75
.eslintrc.js
@@ -3,15 +3,15 @@ module.exports = {
|
||||
ecmaVersion: 6,
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
modules: true
|
||||
modules: true,
|
||||
},
|
||||
sourceType: "module"
|
||||
sourceType: "module",
|
||||
},
|
||||
parser: "babel-eslint",
|
||||
env: {
|
||||
browser: true,
|
||||
es6: true,
|
||||
node: true
|
||||
node: true,
|
||||
},
|
||||
globals: {
|
||||
angular: false,
|
||||
@@ -21,49 +21,63 @@ module.exports = {
|
||||
document: false,
|
||||
expect: false,
|
||||
it: false,
|
||||
require: false
|
||||
require: false,
|
||||
},
|
||||
rules: {
|
||||
"indent": ["error", 2, { "SwitchCase": 1 }],
|
||||
indent: ["warn", 2, { SwitchCase: 1 }],
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"max-lines": ["warn", 300],
|
||||
"max-len": ["warn", { "code": 120, "ignoreComments": true, "ignoreTrailingComments": false }],
|
||||
"max-len": [
|
||||
"warn",
|
||||
{ code: 120, ignoreComments: true, ignoreTrailingComments: false },
|
||||
],
|
||||
"no-console": ["warn"],
|
||||
"no-mixed-operators": ["warn", {
|
||||
"groups": [
|
||||
["==", "!=", "===", "!==", ">", ">=", "<", "<="],
|
||||
["&&", "||"],
|
||||
["in", "instanceof"]
|
||||
],
|
||||
"allowSamePrecedence": true
|
||||
}],
|
||||
"no-mixed-operators": [
|
||||
"warn",
|
||||
{
|
||||
groups: [
|
||||
["==", "!=", "===", "!==", ">", ">=", "<", "<="],
|
||||
["&&", "||"],
|
||||
["in", "instanceof"],
|
||||
],
|
||||
allowSamePrecedence: true,
|
||||
},
|
||||
],
|
||||
"comma-dangle": ["off"],
|
||||
"space-before-function-paren": ["off"],
|
||||
"no-multi-spaces": ["error"],
|
||||
"no-cond-assign": ["warn"],
|
||||
"no-fallthrough": ["warn"],
|
||||
"no-undef": ["error"],
|
||||
"no-unused-vars": ["error"],
|
||||
"no-use-before-define": ["error", { "functions": false, "classes": false, "variables": false }],
|
||||
"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-underscore-dangle": ["warn", { allow: ["_id"] }],
|
||||
"no-redeclare": ["error"],
|
||||
"no-restricted-syntax": ["warn"],
|
||||
"operator-linebreak": ["warn", "before"],
|
||||
"prefer-promise-reject-errors": ["warn"],
|
||||
"padded-blocks": ["warn", { "blocks": "never", "switches": "never", "classes": "never" }],
|
||||
"semi": ["error", "always"],
|
||||
"padded-blocks": [
|
||||
"warn",
|
||||
{ blocks: "never", switches: "never", classes: "never" },
|
||||
],
|
||||
semi: ["error", "always"],
|
||||
"valid-typeof": ["warn"],
|
||||
"no-eval": ["error"],
|
||||
"no-implied-eval": ["error"],
|
||||
"no-debugger": ["warn"],
|
||||
"no-unreachable": ["warn"],
|
||||
"quotes": ["warn", "single", { "avoidEscape": true }],
|
||||
quotes: ["warn", "single", { avoidEscape: true }],
|
||||
"sort-imports": ["off"],
|
||||
"max-lines-per-function": ["off"], // marks the entire functions, a bit too noisy
|
||||
"complexity": ["warn"],
|
||||
"camelcase": ["warn"],
|
||||
"max-statements": ["off"], // marks the entire functions, a bit too noisy
|
||||
complexity: ["warn"],
|
||||
camelcase: ["warn"],
|
||||
"max-statements": ["off"], // marks the entire functions, a bit too noisy
|
||||
"sort-vars": ["off"], // not much value for the work
|
||||
"init-declarations": ["off"],
|
||||
"capitalized-comments": ["off"],
|
||||
@@ -99,13 +113,13 @@ module.exports = {
|
||||
"array-bracket-newline": ["warn"],
|
||||
"array-element-newline": ["warn"],
|
||||
"object-shorthand": ["warn"],
|
||||
"eqeqeq": ["warn"],
|
||||
eqeqeq: ["warn"],
|
||||
"no-empty-function": ["off"],
|
||||
"function-paren-newline": ["warn"],
|
||||
"no-invalid-this": ["warn"],
|
||||
"newline-per-chained-call": ["warn"],
|
||||
"no-unused-expressions": ["warn"],
|
||||
"strict": ["warn"],
|
||||
strict: ["warn"],
|
||||
"no-ternary": ["off"],
|
||||
"multiline-ternary": ["off"],
|
||||
"no-param-reassign": ["error"],
|
||||
@@ -115,9 +129,9 @@ module.exports = {
|
||||
"default-case": ["warn"],
|
||||
"no-alert": ["warn"],
|
||||
"max-params": ["warn"],
|
||||
"brace-style": ["warn", "1tbs", { "allowSingleLine": true }],
|
||||
"brace-style": ["warn", "1tbs", { allowSingleLine: true }],
|
||||
"prefer-const": ["warn"],
|
||||
"class-methods-use-this":["warn"],
|
||||
"class-methods-use-this": ["warn"],
|
||||
// plugin:react
|
||||
"react/jsx-indent": ["warn", 2],
|
||||
"react/jsx-indent-props": ["warn", 2],
|
||||
@@ -131,10 +145,7 @@ module.exports = {
|
||||
"react/jsx-max-depth": ["off"], // rule throws exception in single-dimension-measure
|
||||
"react/jsx-filename-extension": ["warn"],
|
||||
"react/prefer-stateless-function": ["warn"],
|
||||
"react/no-set-state": ["warn"]
|
||||
"react/no-set-state": ["warn"],
|
||||
},
|
||||
extends: [
|
||||
"eslint:all",
|
||||
"plugin:react/all"
|
||||
]
|
||||
}
|
||||
extends: ["eslint:all", "plugin:react/all"],
|
||||
};
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -19,7 +19,7 @@ $RECYCLE.BIN/
|
||||
|
||||
# Temporary build files
|
||||
node_modules/
|
||||
build/
|
||||
dist/
|
||||
BUMPED_VERSION
|
||||
|
||||
# =========================
|
||||
|
||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"json.format.enable": false
|
||||
}
|
||||
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).
|
||||
|
||||
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
|
||||
95
gulpfile.js
95
gulpfile.js
@@ -1,23 +1,62 @@
|
||||
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'
|
||||
},
|
||||
__next: true
|
||||
};
|
||||
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 +75,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,69 +0,0 @@
|
||||
const path = require('path');
|
||||
const settings = require('./settings');
|
||||
|
||||
module.exports = (config) => {
|
||||
config.set({
|
||||
browsers: ['SlimChromeHeadless'],
|
||||
customLaunchers: {
|
||||
SlimChromeHeadless: {
|
||||
base: 'ChromeHeadless',
|
||||
flags: [
|
||||
'--headless',
|
||||
'--disable-gpu',
|
||||
'--disable-translate',
|
||||
'--disable-extensions'
|
||||
]
|
||||
}
|
||||
},
|
||||
files: [
|
||||
{
|
||||
pattern: 'src/**/*.spec.js',
|
||||
watched: true
|
||||
}
|
||||
],
|
||||
frameworks: ['jasmine'],
|
||||
preprocessors: {
|
||||
'src/**/*.spec.{js, jsx}': [
|
||||
'webpack',
|
||||
'sourcemap'
|
||||
]
|
||||
},
|
||||
webpack: {
|
||||
devtool: 'source-map',
|
||||
externals: {
|
||||
jquery: {
|
||||
amd: 'jquery',
|
||||
commonjs: 'jquery',
|
||||
commonjs2: 'jquery',
|
||||
root: '_'
|
||||
}
|
||||
},
|
||||
mode: settings.mode,
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
exclude: [/node_modules/],
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
plugins: [
|
||||
'@babel/plugin-transform-async-to-generator',
|
||||
'@babel/plugin-proposal-class-properties'],
|
||||
presets: ['@babel/preset-react']
|
||||
},
|
||||
test: /\.(js|jsx)$/
|
||||
},
|
||||
{
|
||||
loader: 'ignore-loader',
|
||||
test: /\.less$/
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'test-utilities': path.resolve('test/test-utilities')
|
||||
},
|
||||
modules: ['node_modules']
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
3131
package-lock.json
generated
3131
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
25
package.json
25
package.json
@@ -1,18 +1,15 @@
|
||||
{
|
||||
"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": {
|
||||
@@ -24,7 +21,6 @@
|
||||
"@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",
|
||||
@@ -33,25 +29,16 @@
|
||||
"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",
|
||||
"jasmine-enzyme": "7.0.1",
|
||||
"karma": "3.0.0",
|
||||
"karma-chrome-launcher": "2.2.0",
|
||||
"karma-jasmine": "1.1.2",
|
||||
"karma-sourcemap-loader": "0.3.7",
|
||||
"karma-webpack": "3.0.5",
|
||||
"less": "3.8.1",
|
||||
"less": "3.13.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.
|
Before Width: | Height: | Size: 2.2 KiB |
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,143 +1,92 @@
|
||||
/* eslint-disable react/sort-prop-types */
|
||||
/* eslint-disable space-before-function-paren */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ApplyPreMask } from '../masking';
|
||||
import { addSeparators } from '../utilities';
|
||||
import Tooltip from '../tooltip/index.jsx';
|
||||
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;
|
||||
}
|
||||
class DataCell extends React.PureComponent {
|
||||
constructor (props) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleSelect = this.handleSelect.bind(this);
|
||||
}
|
||||
|
||||
handleSelect () {
|
||||
const { data: { meta: { dimensionCount } }, general: { allowFilteringByClick }, measurement, qlik } = this.props;
|
||||
handleSelect() {
|
||||
const {
|
||||
data: {
|
||||
meta: {
|
||||
dimensionCount
|
||||
}
|
||||
},
|
||||
general: {
|
||||
allowFilteringByClick
|
||||
},
|
||||
measurement,
|
||||
component
|
||||
} = this.props;
|
||||
|
||||
const hasSecondDimension = dimensionCount > 1;
|
||||
if (!allowFilteringByClick) {
|
||||
return;
|
||||
}
|
||||
|
||||
qlik.backendApi.selectValues(0, [measurement.parents.dimension1.elementNumber], true);
|
||||
|
||||
// fixes the console error on selection made from data cells
|
||||
component.selectValues(0, [measurement.parents.dimension1.elementNumber], false);
|
||||
if (hasSecondDimension) {
|
||||
qlik.backendApi.selectValues(1, [measurement.parents.dimension2.elementNumber], true);
|
||||
component.selectValues(1, [measurement.parents.dimension2.elementNumber], false);
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const {
|
||||
data,
|
||||
general,
|
||||
cellWidth,
|
||||
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 textAlignment = styling.options.textAlignment || 'Right';
|
||||
|
||||
let cellStyle = {
|
||||
const cellStyle = {
|
||||
fontFamily: styling.options.fontFamily,
|
||||
...styleBuilder.getStyle(),
|
||||
paddingLeft: '5px',
|
||||
textAlign: textAlignment
|
||||
|
||||
textAlign: textAlignment,
|
||||
minWidth: cellWidth,
|
||||
maxWidth: cellWidth
|
||||
};
|
||||
|
||||
const { semaphoreColors, semaphoreColors: { fieldsToApplyTo } } = styling;
|
||||
const isValidSemaphoreValue = !styleBuilder.hasComments() && !isNaN(measurement.value);
|
||||
const shouldHaveSemaphoreColors = (fieldsToApplyTo.applyToMetric || fieldsToApplyTo.specificFields.indexOf(measurement.parents.dimension1.header) !== -1);
|
||||
if (isValidSemaphoreValue && shouldHaveSemaphoreColors) {
|
||||
const { backgroundColor, color } = getSemaphoreColors(measurement, semaphoreColors);
|
||||
cellStyle = {
|
||||
...styleBuilder.getStyle(),
|
||||
backgroundColor,
|
||||
color,
|
||||
fontFamily: styling.options.fontFamily,
|
||||
paddingLeft: '5px',
|
||||
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 hasTwoDimensions = data.headers.dimension2 && data.headers.dimension2.length > 0;
|
||||
const shouldUseSmallCells = isColumnPercentageBased && data.headers.measurements.length > 1 && hasTwoDimensions;
|
||||
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}
|
||||
>
|
||||
<Tooltip
|
||||
styling={styling}
|
||||
tooltipText={formattedMeasurementValue}
|
||||
>
|
||||
{formattedMeasurementValue}
|
||||
@@ -146,23 +95,31 @@ class DataCell extends React.PureComponent {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DataCell.propTypes = {
|
||||
cellWidth: PropTypes.string.isRequired,
|
||||
data: PropTypes.shape({
|
||||
headers: PropTypes.shape({
|
||||
measurements: PropTypes.array.isRequired
|
||||
}).isRequired,
|
||||
meta: PropTypes.shape({
|
||||
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({
|
||||
component: PropTypes.shape({
|
||||
backendApi: PropTypes.shape({
|
||||
selectValues: PropTypes.func.isRequired
|
||||
selectValues (props, propName) {
|
||||
if (props.isSnapshot || typeof props[propName] === 'function') {
|
||||
return null;
|
||||
}
|
||||
return new Error('Missing implementation of qlik.backendApi.selectValues.');
|
||||
}
|
||||
}).isRequired
|
||||
}).isRequired,
|
||||
styleBuilder: PropTypes.shape({
|
||||
@@ -174,3 +131,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;
|
||||
}
|
||||
|
||||
@@ -5,98 +5,175 @@ import DataCell from './data-cell.jsx';
|
||||
import RowHeader from './row-header.jsx';
|
||||
import { injectSeparators } from '../utilities';
|
||||
|
||||
const DataTable = ({ data, general, qlik, renderData, styling }) => {
|
||||
const {
|
||||
headers: {
|
||||
dimension1,
|
||||
measurements
|
||||
},
|
||||
matrix
|
||||
} = data;
|
||||
// eslint-disable-next-line react/prefer-stateless-function
|
||||
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 {
|
||||
headers: {
|
||||
dimension1,
|
||||
dimension2,
|
||||
measurements
|
||||
},
|
||||
matrix
|
||||
} = data;
|
||||
|
||||
const separatorStyle = {
|
||||
minWidth: columnSeparatorWidth,
|
||||
maxWidth: columnSeparatorWidth
|
||||
};
|
||||
|
||||
const renderMeasurementData = (dimIndex, atEvery) => {
|
||||
if (dimension2.length <= 0) {
|
||||
return injectSeparators(
|
||||
matrix[dimIndex],
|
||||
columnSeparatorWidth,
|
||||
atEvery
|
||||
);
|
||||
}
|
||||
const measurementDataRow = [];
|
||||
let index = 0,
|
||||
match;
|
||||
dimension2.forEach(dim2 => {
|
||||
measurements.forEach((measure, mesInd) => {
|
||||
for (index = 0; index < matrix[dimIndex].length; index++) {
|
||||
match = false;
|
||||
if (
|
||||
matrix[dimIndex][index].parents &&
|
||||
dimension1[dimIndex].displayValue ===
|
||||
matrix[dimIndex][index].parents.dimension1.header
|
||||
) {
|
||||
if (
|
||||
dim2.displayValue ===
|
||||
matrix[dimIndex][index].parents.dimension2.header
|
||||
) {
|
||||
if (
|
||||
measure.name ===
|
||||
matrix[dimIndex][index].parents.measurement.header
|
||||
) {
|
||||
measurementDataRow.push(matrix[dimIndex][index]);
|
||||
match = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
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()
|
||||
};
|
||||
}
|
||||
if (!match) {
|
||||
measurementDataRow.push({
|
||||
displayValue: "",
|
||||
parents: {
|
||||
dimension1: {
|
||||
elementNumber: dimension1[dimIndex].elementNumber,
|
||||
header: dimension1[dimIndex].displayValue
|
||||
},
|
||||
dimension2: {
|
||||
elementNumber: dim2.elementNumber,
|
||||
header: dim2.displayValue
|
||||
},
|
||||
measurement: {
|
||||
header: measure.name,
|
||||
index: mesInd
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
return injectSeparators(
|
||||
measurementDataRow,
|
||||
columnSeparatorWidth,
|
||||
atEvery
|
||||
);
|
||||
};
|
||||
|
||||
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`
|
||||
};
|
||||
|
||||
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}-${dimensionIndex}-separator`}>
|
||||
{!renderData ?
|
||||
<RowHeader
|
||||
component={component}
|
||||
entry={dimensionEntry}
|
||||
rowStyle={rowStyle}
|
||||
styleBuilder={styleBuilder}
|
||||
styling={styling}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
/> : null
|
||||
}
|
||||
{renderData && renderMeasurementData(dimensionIndex, { atEvery: measurements.length }).map((measurementData, index) => {
|
||||
if (measurementData.isSeparator) {
|
||||
return (
|
||||
<td
|
||||
className="empty"
|
||||
key={`${dimensionEntry.displayValue}-${index}-separator`}
|
||||
style={separatorStyle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
return (
|
||||
<DataCell
|
||||
cellWidth={cellWidth}
|
||||
component={component}
|
||||
data={data}
|
||||
general={general}
|
||||
key={`${Math.random()}-${new Date().getTime()}`}
|
||||
measurement={measurementData}
|
||||
styleBuilder={styleBuilder}
|
||||
styling={styling}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DataTable.defaultProps = {
|
||||
renderData: true
|
||||
};
|
||||
|
||||
DataTable.propTypes = {
|
||||
cellWidth: PropTypes.string.isRequired,
|
||||
columnSeparatorWidth: PropTypes.string.isRequired,
|
||||
component: PropTypes.shape({}).isRequired,
|
||||
data: PropTypes.shape({
|
||||
headers: PropTypes.shape({
|
||||
dimension1: PropTypes.array.isRequired
|
||||
@@ -104,7 +181,6 @@ DataTable.propTypes = {
|
||||
matrix: PropTypes.arrayOf(PropTypes.array.isRequired).isRequired
|
||||
}).isRequired,
|
||||
general: PropTypes.shape({}).isRequired,
|
||||
qlik: PropTypes.shape({}).isRequired,
|
||||
renderData: PropTypes.bool,
|
||||
styling: PropTypes.shape({
|
||||
hasCustomFileStyle: PropTypes.bool.isRequired
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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) {
|
||||
@@ -9,38 +10,53 @@ class RowHeader extends React.PureComponent {
|
||||
this.handleSelect = this.handleSelect.bind(this);
|
||||
}
|
||||
|
||||
// fixes the console error on row selected values
|
||||
handleSelect () {
|
||||
const { entry, qlik } = this.props;
|
||||
qlik.backendApi.selectValues(0, [entry.elementNumber], true);
|
||||
const { component, entry } = this.props;
|
||||
component.selectValues(0, [entry.elementNumber], 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RowHeader.propTypes = {
|
||||
entry: PropTypes.shape({
|
||||
displayValue: PropTypes.string.isRequired
|
||||
}).isRequired,
|
||||
qlik: PropTypes.shape({
|
||||
component: PropTypes.shape({
|
||||
backendApi: PropTypes.shape({
|
||||
selectValues: PropTypes.func.isRequired
|
||||
selectValues (props, propName) {
|
||||
if (props.isSnapshot || typeof props[propName] === 'function') {
|
||||
return null;
|
||||
}
|
||||
return new Error('Missing implementation of qlik.backendApi.selectValues.');
|
||||
}
|
||||
}).isRequired
|
||||
}).isRequired,
|
||||
entry: PropTypes.shape({
|
||||
displayValue: PropTypes.string.isRequired,
|
||||
elementNumber: PropTypes.number.isRequired
|
||||
}).isRequired,
|
||||
rowStyle: PropTypes.shape({}).isRequired,
|
||||
styleBuilder: PropTypes.shape({}).isRequired,
|
||||
styling: PropTypes.shape({}).isRequired
|
||||
|
||||
91
src/dataset.js
Normal file
91
src/dataset.js
Normal file
@@ -0,0 +1,91 @@
|
||||
import qlik from 'qlik';
|
||||
|
||||
function createCube (definition, app) {
|
||||
return new Promise(resolve => {
|
||||
app.createCube(definition, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
async function buildDataCube (originCubeDefinition, originCube, app, requestPage) {
|
||||
const cubeDefinition = {
|
||||
...originCubeDefinition,
|
||||
qInitialDataFetch: [
|
||||
{
|
||||
// eslint-disable-next-line no-undefined
|
||||
qTop: requestPage === undefined ? 0 : requestPage[0].qTop,
|
||||
qLeft: 0,
|
||||
qHeight: 1000,
|
||||
qWidth: originCube.qSize.qcx
|
||||
}
|
||||
],
|
||||
qDimensions: [originCubeDefinition.qDimensions[0]],
|
||||
qMeasures: originCubeDefinition.qMeasures
|
||||
};
|
||||
if (originCube.qDimensionInfo.length === 2) {
|
||||
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());
|
||||
const rowCount = component.backendApi.getRowCount();
|
||||
const cellCount = rowCount * layout.qHyperCube.qSize.qcx;
|
||||
const maxLoops = layout.maxloops;
|
||||
|
||||
// If this is a master object, fetch the hyperCubeDef of the original object
|
||||
let hyperCubeDef = properties.qExtendsId
|
||||
? (await app.getObjectProperties(properties.qExtendsId)).properties.qHyperCubeDef
|
||||
: properties.qHyperCubeDef;
|
||||
hyperCubeDef = JSON.parse(JSON.stringify(hyperCubeDef));
|
||||
hyperCubeDef.qStateName = layout.qStateName;
|
||||
const pagedCube = {};
|
||||
let lastRow = 0;
|
||||
if (cellCount < (maxLoops * 10000)) {
|
||||
for (let index = 0; cellCount > lastRow; index += 1) {
|
||||
const requestPage = [
|
||||
{
|
||||
qHeight: 1000,
|
||||
qLeft: 0,
|
||||
qTop: lastRow,
|
||||
qWidth: 10 // should be # of columns
|
||||
}
|
||||
];
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
pagedCube[index] = await buildDataCube(hyperCubeDef, layout.qHyperCube, app, requestPage);
|
||||
lastRow = lastRow + 1000;
|
||||
}
|
||||
return pagedCube;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function initializeDesignList (component, layout) {
|
||||
if (component.backendApi.isSnapshot) {
|
||||
return layout.snapshotData.designList;
|
||||
}
|
||||
|
||||
if (!layout.stylingfield) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return 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({ rows: 5000 });
|
||||
});
|
||||
}
|
||||
@@ -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 chart which allows you to display Profit & Loss
|
||||
reporting with color and font customizations.`,
|
||||
component: 'text'
|
||||
},
|
||||
paragraph2: {
|
||||
label: 'P&L pivot is based upon an extension created by Ivan Felipe Asensio.',
|
||||
component: 'text'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
type: 'items'
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
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,
|
||||
@@ -23,30 +23,6 @@ const pagination = {
|
||||
{
|
||||
value: 4,
|
||||
label: '40k cells'
|
||||
},
|
||||
{
|
||||
value: 5,
|
||||
label: '50k cells'
|
||||
},
|
||||
{
|
||||
value: 6,
|
||||
label: '60k cells'
|
||||
},
|
||||
{
|
||||
value: 7,
|
||||
label: '70k cells'
|
||||
},
|
||||
{
|
||||
value: 8,
|
||||
label: '80k cells'
|
||||
},
|
||||
{
|
||||
value: 9,
|
||||
label: '90k cells'
|
||||
},
|
||||
{
|
||||
value: 10,
|
||||
label: '100k cells'
|
||||
}
|
||||
],
|
||||
defaultValue: 2
|
||||
@@ -55,7 +31,8 @@ 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.
|
||||
Change the pagination size supported or 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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { exportXLS } from './excel-export';
|
||||
|
||||
class ExportButton extends React.PureComponent {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.handleExport = this.handleExport.bind(this);
|
||||
}
|
||||
|
||||
handleExport () {
|
||||
const { excelExport, general } = this.props;
|
||||
const { title, subtitle, footnote } = general;
|
||||
if (excelExport) {
|
||||
exportXLS(title, subtitle, footnote);
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { excelExport } = this.props;
|
||||
return excelExport === true && (
|
||||
<input
|
||||
className="icon-xls"
|
||||
onClick={this.handleExport}
|
||||
src="/Extensions/qlik-smart-pivot/Excel.png"
|
||||
type="image"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ExportButton.defaultProps = {
|
||||
excelExport: false
|
||||
};
|
||||
|
||||
ExportButton.propTypes = {
|
||||
excelExport: PropTypes.bool,
|
||||
general: PropTypes.shape({}).isRequired
|
||||
};
|
||||
|
||||
export default ExportButton;
|
||||
@@ -1,38 +1,46 @@
|
||||
/* eslint-disable object-shorthand */
|
||||
/* eslint-disable space-before-function-paren */
|
||||
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) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleSelect = this.handleSelect.bind(this);
|
||||
}
|
||||
|
||||
handleSelect () {
|
||||
const { entry, qlik } = this.props;
|
||||
qlik.backendApi.selectValues(1, [entry.elementNumber], true);
|
||||
// fixes console error for column selected values
|
||||
handleSelect() {
|
||||
const { component, entry } = this.props;
|
||||
component.selectValues(1, [entry.elementNumber], false);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { baseCSS, cellSuffix, colSpan, entry, styling, qlik } = this.props;
|
||||
const inEditState = qlik.inEditState();
|
||||
render() {
|
||||
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}
|
||||
>
|
||||
<Tooltip
|
||||
isTooltipActive={!inEditState}
|
||||
styling={styling}
|
||||
tooltipText={entry.displayValue}
|
||||
>
|
||||
{entry.displayValue}
|
||||
@@ -43,23 +51,27 @@ class ColumnHeader extends React.PureComponent {
|
||||
}
|
||||
|
||||
ColumnHeader.defaultProps = {
|
||||
cellSuffix: '',
|
||||
colSpan: 1
|
||||
};
|
||||
|
||||
ColumnHeader.propTypes = {
|
||||
baseCSS: PropTypes.shape({}).isRequired,
|
||||
cellSuffix: PropTypes.string,
|
||||
cellWidth: PropTypes.string.isRequired,
|
||||
colSpan: PropTypes.number,
|
||||
entry: PropTypes.shape({
|
||||
elementNumber: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired
|
||||
}).isRequired,
|
||||
qlik: PropTypes.shape({
|
||||
component: PropTypes.shape({
|
||||
backendApi: PropTypes.shape({
|
||||
selectValues: PropTypes.func.isRequired
|
||||
selectValues: function (props, propName) {
|
||||
if (props.isSnapshot || typeof props[propName] === 'function') {
|
||||
return null;
|
||||
}
|
||||
return new Error('Missing implementation of qlik.backendApi.selectValues.');
|
||||
}
|
||||
}).isRequired
|
||||
}).isRequired,
|
||||
entry: PropTypes.shape({
|
||||
displayValue: PropTypes.string.isRequired,
|
||||
elementNumber: PropTypes.number.isRequired
|
||||
}).isRequired,
|
||||
styling: PropTypes.shape({
|
||||
headerOptions: PropTypes.shape({
|
||||
fontSizeAdjustment: PropTypes.number.isRequired
|
||||
|
||||
48
src/headers-table/dim1-header.jsx
Normal file
48
src/headers-table/dim1-header.jsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { HEADER_FONT_SIZE } from '../initialize-transformed';
|
||||
import Tooltip from '../tooltip/index.jsx';
|
||||
|
||||
const Dim1Header = ({ component, baseCSS, title, hasSecondDimension, styling }) => {
|
||||
const inEditState = component.inEditState();
|
||||
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: isMediumFontSize ? '90px' : '70px',
|
||||
verticalAlign: 'middle',
|
||||
width: '230px'
|
||||
};
|
||||
|
||||
return (
|
||||
<th
|
||||
className="fdim-cells"
|
||||
rowSpan={rowSpan}
|
||||
style={style}
|
||||
>
|
||||
<Tooltip
|
||||
isTooltipActive={!inEditState}
|
||||
styling={styling}
|
||||
tooltipText={title}
|
||||
>
|
||||
{title}
|
||||
</Tooltip>
|
||||
</th>
|
||||
);
|
||||
};
|
||||
|
||||
Dim1Header.propTypes = {
|
||||
baseCSS: PropTypes.shape({}).isRequired,
|
||||
component: PropTypes.shape({}).isRequired,
|
||||
hasSecondDimension: PropTypes.bool.isRequired,
|
||||
styling: PropTypes.shape({
|
||||
headerOptions: PropTypes.shape({
|
||||
fontSizeAdjustment: PropTypes.number.isRequired
|
||||
}).isRequired
|
||||
}).isRequired,
|
||||
title: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default Dim1Header;
|
||||
@@ -1,44 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ExportButton from '../export-button.jsx';
|
||||
|
||||
const ExportColumnHeader = ({ baseCSS, general, title, allowExcelExport, hasSecondDimension, styling }) => {
|
||||
const rowSpan = hasSecondDimension ? 2 : 1;
|
||||
const style = {
|
||||
...baseCSS,
|
||||
cursor: 'default',
|
||||
fontSize: `${16 + styling.headerOptions.fontSizeAdjustment} px`,
|
||||
height: '80px',
|
||||
verticalAlign: 'middle',
|
||||
width: '230px'
|
||||
};
|
||||
|
||||
return (
|
||||
<th
|
||||
className="fdim-cells"
|
||||
rowSpan={rowSpan}
|
||||
style={style}
|
||||
>
|
||||
<ExportButton
|
||||
excelExport={allowExcelExport}
|
||||
general={general}
|
||||
/>
|
||||
{title}
|
||||
</th>
|
||||
);
|
||||
};
|
||||
|
||||
ExportColumnHeader.propTypes = {
|
||||
allowExcelExport: PropTypes.bool.isRequired,
|
||||
baseCSS: PropTypes.shape({}).isRequired,
|
||||
general: PropTypes.shape({}).isRequired,
|
||||
hasSecondDimension: PropTypes.bool.isRequired,
|
||||
styling: PropTypes.shape({
|
||||
headerOptions: PropTypes.shape({
|
||||
fontSizeAdjustment: PropTypes.number.isRequired
|
||||
}).isRequired
|
||||
}).isRequired,
|
||||
title: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default ExportColumnHeader;
|
||||
@@ -1,121 +1,124 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ExportColumnHeader from './export-column-header.jsx';
|
||||
import Dim1Header from './dim1-header.jsx';
|
||||
import ColumnHeader from './column-header.jsx';
|
||||
import 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,
|
||||
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 ?
|
||||
<Dim1Header
|
||||
baseCSS={baseCSS}
|
||||
cellSuffix={general.cellSuffix}
|
||||
colSpan={measurements.length}
|
||||
entry={entry}
|
||||
key={entry.displayValue}
|
||||
qlik={qlik}
|
||||
component={component}
|
||||
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
|
||||
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}-${index}-separator`}
|
||||
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,
|
||||
@@ -123,16 +126,12 @@ HeadersTable.propTypes = {
|
||||
measurements: PropTypes.array
|
||||
})
|
||||
}).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;
|
||||
|
||||
@@ -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
|
||||
|
||||
129
src/index.js
129
src/index.js
@@ -1,40 +1,58 @@
|
||||
import paint from './paint.jsx';
|
||||
import definition from './definition';
|
||||
import './main.less';
|
||||
import definition from "./definition";
|
||||
import { exportXLS } from "./excel-export";
|
||||
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
|
||||
require('@babel/polyfill'); // eslint-disable-line global-require
|
||||
if (!window._babelPolyfill) {
|
||||
// eslint-disable-line no-underscore-dangle
|
||||
require("@babel/polyfill"); // eslint-disable-line global-require
|
||||
}
|
||||
|
||||
export default {
|
||||
controller: [
|
||||
'$scope',
|
||||
'$timeout',
|
||||
function () { }
|
||||
],
|
||||
export default ({ flags }) => ({
|
||||
design: {
|
||||
dimensions: {
|
||||
max: 1,
|
||||
min: 0
|
||||
}
|
||||
},
|
||||
data: {
|
||||
dimensions: {
|
||||
max: 2,
|
||||
max (nMeasures) {
|
||||
return nMeasures < 9 ? 2 : 1;
|
||||
},
|
||||
min: 1,
|
||||
uses: 'dimensions'
|
||||
},
|
||||
measures: {
|
||||
max: 9,
|
||||
max (nDims) {
|
||||
return nDims < 2 ? 9 : 8;
|
||||
},
|
||||
min: 1,
|
||||
uses: 'measures'
|
||||
}
|
||||
},
|
||||
// Prevent conversion from and to this object
|
||||
exportProperties: null,
|
||||
importProperties: null,
|
||||
definition,
|
||||
initialProperties: {
|
||||
version: 1.0,
|
||||
qHyperCubeDef: {
|
||||
qDimensions: [],
|
||||
qInitialDataFetch: [
|
||||
{
|
||||
qHeight: 1000,
|
||||
qWidth: 10
|
||||
qTop: 0,
|
||||
qLeft: 0,
|
||||
qWidth: 50,
|
||||
qHeight: 50
|
||||
}
|
||||
],
|
||||
qMeasures: []
|
||||
qMeasures: [],
|
||||
qSuppressZero: false
|
||||
}
|
||||
},
|
||||
support: {
|
||||
@@ -42,16 +60,81 @@ 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;
|
||||
async paint ($element, layout, requestPage) {
|
||||
const dataCube = await initializeDataCube(this, layout, requestPage);
|
||||
const editmodeClass = this.inAnalysisState() ? '' : 'edit-mode';
|
||||
let state, designList;
|
||||
if (dataCube === null) {
|
||||
state = {
|
||||
$element,
|
||||
component: this,
|
||||
dataCube,
|
||||
designList,
|
||||
layout,
|
||||
error: true
|
||||
};
|
||||
} else {
|
||||
designList = await initializeDesignList(this, layout);
|
||||
state = await initializeStore({
|
||||
$element,
|
||||
component: this,
|
||||
dataCube,
|
||||
designList,
|
||||
layout,
|
||||
error: false
|
||||
});
|
||||
}
|
||||
const jsx = (
|
||||
<Root editmodeClass={editmodeClass} component={this} state={state} />
|
||||
);
|
||||
|
||||
ReactDOM.render(jsx, $element[0]);
|
||||
},
|
||||
snapshot: {
|
||||
canTakeSnapshot: true
|
||||
},
|
||||
async setSnapshotData (snapshotLayout) {
|
||||
snapshotLayout.snapshotData.dataCube = await initializeDataCube(
|
||||
this,
|
||||
snapshotLayout
|
||||
);
|
||||
snapshotLayout.snapshotData.designList = await initializeDesignList(
|
||||
this,
|
||||
snapshotLayout
|
||||
);
|
||||
return snapshotLayout;
|
||||
},
|
||||
async getContextMenu (obj, menu) {
|
||||
if (!this.$scope.layout.allowexportxls) {
|
||||
return menu;
|
||||
}
|
||||
|
||||
const app = this.backendApi.model.app;
|
||||
const isPersonalResult = await app.global.isPersonalMode();
|
||||
|
||||
// Export as XLS option is removed from desktop because the desktop wrapper blocks downloads.
|
||||
// Enabled for windows and QCS
|
||||
// isPersonalMode returns true for both desktop and QCS
|
||||
// By checking both if has download dialog and if is QCS can enable Export as XLS option on QCS
|
||||
if (
|
||||
(this.backendApi.model.layout.qMeta.privileges.includes('exportdata') && !isPersonalResult) ||
|
||||
(flags.isEnabled('DOWNLOAD_USE_REPORTING') && isPersonalResult)
|
||||
) {
|
||||
menu.addItem({
|
||||
translation: 'Export as XLS',
|
||||
tid: 'export-excel',
|
||||
icon: 'export',
|
||||
select: () => {
|
||||
exportXLS(
|
||||
this.$element,
|
||||
this.$scope.layout.title,
|
||||
this.$scope.layout.subtitle,
|
||||
this.$scope.layout.footnote
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
return menu;
|
||||
},
|
||||
version: 1.0
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,346 +1,300 @@
|
||||
import jQuery from 'jquery';
|
||||
import { distinctArray } from './utilities';
|
||||
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) {
|
||||
function getAlignment(option) {
|
||||
const alignmentOptions = {
|
||||
1: 'left',
|
||||
2: 'center',
|
||||
3: 'right'
|
||||
1: "left",
|
||||
2: "center",
|
||||
3: "right",
|
||||
};
|
||||
|
||||
return alignmentOptions[option] || 'left';
|
||||
return alignmentOptions[option] || "left";
|
||||
}
|
||||
|
||||
function getFontSizeAdjustment (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();
|
||||
function generateMeasurements(information) {
|
||||
return information.map((measurement) => {
|
||||
const transformedMeasurement = {
|
||||
format,
|
||||
magnitudeLabelSuffix: getMagnitudeLabelSuffix(formatMagnitude),
|
||||
name: measurement.qFallbackTitle
|
||||
format: measurement.qNumFormat.qFmt || "#.##0",
|
||||
name: measurement.qFallbackTitle,
|
||||
};
|
||||
|
||||
return transformedMeasurement;
|
||||
});
|
||||
}
|
||||
|
||||
function generateDimensionEntry (information, data) {
|
||||
function generateDimensionEntry(information, data) {
|
||||
return {
|
||||
displayValue: data.qText,
|
||||
displayValue: data.qText || data.qNum,
|
||||
elementNumber: data.qElemNumber,
|
||||
name: information.qFallbackTitle,
|
||||
value: data.qNum
|
||||
value: data.qNum,
|
||||
};
|
||||
}
|
||||
|
||||
function generateMatrixCell ({ cell, dimension1Information, dimension2Information, measurementInformation }) {
|
||||
function generateMatrixCell({
|
||||
cell,
|
||||
dimension1Information,
|
||||
dimension2Information,
|
||||
measurementInformation,
|
||||
}) {
|
||||
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: {
|
||||
elementNumber: dimension1Information.qElemNumber,
|
||||
header: dimension1Information.qText
|
||||
header: dimension1Information.qText,
|
||||
},
|
||||
measurement: {
|
||||
header: measurementInformation.name
|
||||
}
|
||||
header: measurementInformation.name,
|
||||
index: measurementInformation.index,
|
||||
},
|
||||
},
|
||||
value: cell.qNum
|
||||
value: cell.qNum,
|
||||
};
|
||||
|
||||
if (dimension2Information) {
|
||||
matrixCell.parents.dimension2 = {
|
||||
elementNumber: dimension2Information.qElemNumber
|
||||
elementNumber: dimension2Information.qElemNumber,
|
||||
header: dimension2Information.qText,
|
||||
};
|
||||
}
|
||||
|
||||
return matrixCell;
|
||||
}
|
||||
|
||||
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) => {
|
||||
lastRow += 1;
|
||||
const dimension1Entry = generateDimensionEntry(dimensionsInformation[0], row[0]);
|
||||
dimension1.push(dimension1Entry);
|
||||
let dimension2Entry;
|
||||
let firstDataCell = 1;
|
||||
if (hasSecondDimension) {
|
||||
dimension2Entry = generateDimensionEntry(dimensionsInformation[1], row[1]);
|
||||
dimension2.push(dimension2Entry);
|
||||
firstDataCell = 2;
|
||||
}
|
||||
const matrixRow = row
|
||||
.slice(firstDataCell, row.length)
|
||||
.map((cell, cellIndex) => {
|
||||
const measurementInformation = measurements[cellIndex];
|
||||
const dimension1Information = row[0]; // eslint-disable-line prefer-destructuring
|
||||
const dimension2Information = hasSecondDimension ? row[1] : null;
|
||||
const generatedCell = generateMatrixCell({
|
||||
cell,
|
||||
dimension1Information,
|
||||
dimension2Information,
|
||||
measurementInformation
|
||||
// eslint-disable-next-line no-undefined
|
||||
for (let index = 0; dataCube[index] !== undefined; index++) {
|
||||
// eslint-disable-next-line no-loop-func
|
||||
dataCube[index].forEach((row) => {
|
||||
const dimension1Entry = generateDimensionEntry(
|
||||
dimensionsInformation[0],
|
||||
row[0]
|
||||
);
|
||||
dimension1.push(dimension1Entry);
|
||||
let dimension2Entry;
|
||||
let firstDataCell = 1;
|
||||
if (hasSecondDimension) {
|
||||
dimension2Entry = generateDimensionEntry(
|
||||
dimensionsInformation[1],
|
||||
row[1]
|
||||
);
|
||||
dimension2.push(dimension2Entry);
|
||||
firstDataCell = 2;
|
||||
}
|
||||
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({
|
||||
cell,
|
||||
dimension1Information,
|
||||
dimension2Information,
|
||||
measurementInformation,
|
||||
});
|
||||
return generatedCell;
|
||||
});
|
||||
|
||||
return generatedCell;
|
||||
});
|
||||
|
||||
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;
|
||||
let appendToRowIndex = matrix.length;
|
||||
if (hasSecondDimension) {
|
||||
// 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);
|
||||
|
||||
return {
|
||||
dimension1: distinctArray(dimension1),
|
||||
dimension2: distinctArray(dimension2),
|
||||
dimension1: dimension1,
|
||||
dimension2: dimension2,
|
||||
matrix,
|
||||
measurements
|
||||
measurements,
|
||||
};
|
||||
}
|
||||
|
||||
async function initializeTransformed ({ $element, layout, component }) {
|
||||
const colors = initializeColors({ layout });
|
||||
function initializeTransformed({
|
||||
$element,
|
||||
component,
|
||||
dataCube,
|
||||
designList,
|
||||
layout,
|
||||
}) {
|
||||
const dimensionsInformation = component.backendApi.getDimensionInfos();
|
||||
const measurementsInformation = component.backendApi.getMeasureInfos();
|
||||
const dimensionCount = layout.qHyperCube.qDimensionInfo.length;
|
||||
const rowCount = component.backendApi.getRowCount();
|
||||
const maxLoops = layout.maxloops;
|
||||
const {
|
||||
dimension1,
|
||||
dimension2,
|
||||
measurements,
|
||||
matrix
|
||||
} = generateDataSet(component, dimensionsInformation, measurementsInformation);
|
||||
const { dimension1, dimension2, measurements, matrix } = 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)
|
||||
const transformedProperties = {
|
||||
element: $element[0],
|
||||
data: {
|
||||
headers: {
|
||||
dimension1, // column headers
|
||||
dimension2, // parent row headers if exists
|
||||
measurements // row headers, looped for each dimension2 if exists
|
||||
measurements, // row headers, looped for each dimension2 if exists
|
||||
},
|
||||
matrix, // 2d array of all rows/cells to render in body of datatable
|
||||
meta: {
|
||||
dimensionCount: dimensionsInformation.length
|
||||
}
|
||||
dimensionCount: dimensionsInformation.length,
|
||||
},
|
||||
},
|
||||
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)
|
||||
dimensionSelectionCounts: dimensionsInformation.map(
|
||||
(dimensionInfo) => dimensionInfo.qStateCounts.qSelected
|
||||
),
|
||||
},
|
||||
styling: {
|
||||
colors,
|
||||
customCSV: {
|
||||
basic: customSchemaBasic,
|
||||
count: customHeadersCount,
|
||||
full: customSchemaFull
|
||||
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.ColorSchema}P`],
|
||||
backgroundColor: layout.rowEvenBGColor,
|
||||
backgroundColorOdd: layout.rowOddBGColor,
|
||||
color: layout.BodyTextColorSchema,
|
||||
fontFamily: layout.FontFamily,
|
||||
fontSizeAdjustment: getFontSizeAdjustment(layout.lettersize),
|
||||
textAlignment: layout.cellTextAlignment
|
||||
textAlignment: layout.cellTextAlignment,
|
||||
},
|
||||
semaphoreColors: {
|
||||
fieldsToApplyTo: {
|
||||
applyToAll: layout.allsemaphores,
|
||||
applyToMetric: layout.allmetrics,
|
||||
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
|
||||
colors: {
|
||||
poor: {
|
||||
color: layout.conditionalcoloring.color_poor,
|
||||
textColor: layout.conditionalcoloring.textcolor_poor,
|
||||
},
|
||||
fair: {
|
||||
color: layout.conditionalcoloring.color_fair,
|
||||
textColor: layout.conditionalcoloring.textcolor_fair,
|
||||
},
|
||||
good: {
|
||||
color: layout.conditionalcoloring.color_good,
|
||||
textColor: layout.conditionalcoloring.textcolor_good,
|
||||
},
|
||||
},
|
||||
statusColors: {
|
||||
critical: {
|
||||
backgroundColor: layout.colorstatus1.color,
|
||||
color: layout.colorstatus1text.color
|
||||
},
|
||||
medium: {
|
||||
backgroundColor: layout.colorstatus2.color,
|
||||
color: layout.colorstatus2text.color
|
||||
},
|
||||
normal: {
|
||||
backgroundColor: layout.colorstatus3.color,
|
||||
color: layout.colorstatus3text.color
|
||||
}
|
||||
}
|
||||
},
|
||||
symbolForNulls: layout.symbolfornulls,
|
||||
usePadding: layout.indentbool,
|
||||
useSeparatorColumns: dimensionCount === 1 ? false : layout.separatorcols
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if (rowCount > lastRow && rowCount <= (maxLoops * 1000)) {
|
||||
const requestPage = [
|
||||
{
|
||||
qHeight: Math.min(1000, rowCount - lastRow),
|
||||
qLeft: 0,
|
||||
qTop: matrix.length,
|
||||
qWidth: 10 // should be # of columns
|
||||
}
|
||||
];
|
||||
component.backendApi.getData(requestPage).then(() => {
|
||||
component.paint($element, layout);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return transformedProperties;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ class LinkedScrollWrapper extends React.PureComponent {
|
||||
unlinkComponent (component) {
|
||||
const componentIndex = this.scrollElements.map(element => element.component).indexOf(component);
|
||||
if (componentIndex !== -1) {
|
||||
this.scrollElements.removeAt(componentIndex);
|
||||
this.scrollElements.splice(componentIndex, 1);
|
||||
// eslint-disable-next-line react/no-find-dom-node
|
||||
const node = ReactDOM.findDOMNode(component);
|
||||
node.onscroll = null;
|
||||
|
||||
324
src/main.less
324
src/main.less
@@ -1,6 +1,5 @@
|
||||
/* eslint-disable */
|
||||
.qv-object-qlik-smart-pivot {
|
||||
@TableBorder: 1px solid #d3d3d3;
|
||||
@KpiTableWidth: 230px;
|
||||
|
||||
*,
|
||||
@@ -13,9 +12,7 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
._cell(@Width: 50px) {
|
||||
min-width: @Width !important;
|
||||
max-width: @Width !important;
|
||||
.grid-cells {
|
||||
cursor: pointer;
|
||||
line-height: 1em !important;
|
||||
}
|
||||
@@ -24,38 +21,6 @@
|
||||
z-index: 110;
|
||||
}
|
||||
|
||||
.tooltip-wrapper {
|
||||
min-width: 25px;
|
||||
position: fixed;
|
||||
padding: 5px;
|
||||
padding-top: 15px;
|
||||
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%);
|
||||
|
||||
> p {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-wrapper::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;
|
||||
}
|
||||
|
||||
.icon-xls {
|
||||
text-align: left;
|
||||
}
|
||||
@@ -65,143 +30,47 @@
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
border-collapse: separate;
|
||||
border-spacing: 1px;
|
||||
width: auto;
|
||||
border-left: @TableBorder;
|
||||
border-right: @TableBorder;
|
||||
border-top: @TableBorder;
|
||||
}
|
||||
|
||||
tr {
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
border: 1px solid #fff;
|
||||
padding: 5px;
|
||||
border-collapse: collapse;
|
||||
padding: 5px !important; // prevent overwriting from single object
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: middle;
|
||||
cursor: default;
|
||||
|
||||
> div {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-family: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.empty {
|
||||
width: 3%;
|
||||
background: #fff;
|
||||
min-width: 4px !important;
|
||||
max-width: 4px !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
th.main-kpi {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
border-bottom: @TableBorder;
|
||||
}
|
||||
|
||||
.numeric {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* This is for wrap text in headers */
|
||||
.wrapclass25 {
|
||||
width: 100%;
|
||||
height: 25px;
|
||||
white-space: pre-line;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.wrapclass45 {
|
||||
width: 100%;
|
||||
height: 45px;
|
||||
white-space: pre-line;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.wrapclass70 {
|
||||
width: 100%;
|
||||
height: 70px;
|
||||
white-space: pre-line;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.wrapclassEmpty {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ***************** */
|
||||
|
||||
/* Medium column size */
|
||||
|
||||
/* ***************** */
|
||||
|
||||
.grid-cells {
|
||||
position: relative;
|
||||
._cell(70px);
|
||||
}
|
||||
|
||||
.grid-cells2 {
|
||||
._cell(70px);
|
||||
}
|
||||
|
||||
.grid-cells-small {
|
||||
._cell(52px);
|
||||
}
|
||||
|
||||
.grid-cells2-small {
|
||||
._cell(52px);
|
||||
}
|
||||
|
||||
/* ***************** */
|
||||
|
||||
/* Small column size */
|
||||
|
||||
/* ***************** */
|
||||
.grid-cells-s {
|
||||
._cell(67px);
|
||||
}
|
||||
|
||||
.grid-cells2-s {
|
||||
._cell(67px);
|
||||
}
|
||||
|
||||
.grid-cells-small-s {
|
||||
._cell(52px);
|
||||
}
|
||||
|
||||
.grid-cells2-small-s {
|
||||
._cell(52px);
|
||||
}
|
||||
|
||||
/* ***************** */
|
||||
|
||||
/* large column size */
|
||||
|
||||
/* ***************** */
|
||||
|
||||
.grid-cells-l {
|
||||
._cell(82px);
|
||||
}
|
||||
|
||||
.grid-cells2-l {
|
||||
._cell(82px);
|
||||
}
|
||||
|
||||
.grid-cells-small-l {
|
||||
._cell(66px);
|
||||
}
|
||||
|
||||
.grid-cells2-small-l {
|
||||
._cell(66px);
|
||||
}
|
||||
|
||||
/* END OF GRID CELLS */
|
||||
|
||||
/* First Column */
|
||||
// First Column
|
||||
.fdim-cells {
|
||||
min-width: 230px !Important;
|
||||
max-width: 230px !Important;
|
||||
@@ -209,29 +78,12 @@
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.fdim-cells:hover {
|
||||
background-color: #808080 !important;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
tbody tr:hover {
|
||||
tbody tr:hover td {
|
||||
cursor: default;
|
||||
background-color: #808080 !important;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.grid-cells-header {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.grid-cells-title {
|
||||
min-width: 522px;
|
||||
}
|
||||
|
||||
.grid-cells::before {
|
||||
content: "\00a0";
|
||||
}
|
||||
|
||||
.grid {
|
||||
height: 50px;
|
||||
width: 350px;
|
||||
@@ -245,62 +97,118 @@
|
||||
border: groove;
|
||||
}
|
||||
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.error {
|
||||
position: absolute; /*Define position */
|
||||
width: 100%; /* Full width (cover the whole page) */
|
||||
height: 100%; /* Full height (cover the whole page) */
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000; /* Specify a stack order in case you're using a different order for other elements */
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.kpi-table .fdim-cells,
|
||||
.data-table td {
|
||||
line-height: 1em !important;
|
||||
}
|
||||
|
||||
.data-table .fdim-cells {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.kpi-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
flex: none;
|
||||
width: @KpiTableWidth !important;
|
||||
overflow: hidden !important;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-right: 1px solid #fff;
|
||||
box-shadow: 4px 2px 8px #e1e1e1;
|
||||
|
||||
.header-wrapper {
|
||||
flex: none;
|
||||
box-shadow: 4px 2px 8px #e1e1e1;
|
||||
}
|
||||
|
||||
.row-wrapper {
|
||||
height: calc(~"100% - 97px");
|
||||
overflow: scroll;
|
||||
position: absolute;
|
||||
margin: 0;
|
||||
margin-bottom: 8px;
|
||||
padding: 0;
|
||||
margin-top: 0;
|
||||
box-shadow: 4px 2px 8px #e1e1e1;
|
||||
min-height: 0; /* This is to make flex size-filling work */
|
||||
|
||||
/* Adapt for Edge */
|
||||
@supports (-ms-ime-align: auto) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* Adapt for IE11 */
|
||||
@media screen and (-ms-high-contrast: none) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin-top: 0;
|
||||
min-height: 0; /* This is to make flex size-filling work */
|
||||
|
||||
/* Style scrollbar for FF */
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #d3d3d3 transparent;
|
||||
}
|
||||
|
||||
/* Adapt for Edge */
|
||||
@supports (-ms-ime-align: auto) {
|
||||
.header-wrapper {
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Adapt for IE11 */
|
||||
@media screen and (-ms-high-contrast: none) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.header-wrapper {
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hide scrollbars
|
||||
.kpi-table .header-wrapper,
|
||||
.kpi-table .row-wrapper,
|
||||
.data-table .header-wrapper,
|
||||
.data-table .row-wrapper {
|
||||
/* stylelint-disable-next-line property-no-unknown */
|
||||
.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
|
||||
@@ -309,4 +217,38 @@
|
||||
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 */
|
||||
|
||||
132
src/masking.js
132
src/masking.js
@@ -1,132 +0,0 @@
|
||||
import { addSeparators } from './utilities';
|
||||
|
||||
export function ApplyPreMask (mask, value) { // aqui
|
||||
if (mask.indexOf(';') >= 0) {
|
||||
if (value >= 0) {
|
||||
switch (mask.substring(0, mask.indexOf(';'))) {
|
||||
case '#.##0':
|
||||
return (addSeparators(value, '.', ',', 0));
|
||||
case '#,##0':
|
||||
return (addSeparators(value, ',', '.', 0));
|
||||
case '+#.##0':
|
||||
return (addSeparators(value, '.', ',', 0));
|
||||
case '+#,##0':
|
||||
return (addSeparators(value, ',', '.', 0));
|
||||
default:
|
||||
return (applyMask(mask.substring(0, mask.indexOf(';')), value));
|
||||
}
|
||||
} else {
|
||||
const vMyValue = value * -1;
|
||||
let vMyMask = mask.substring(mask.indexOf(';') + 1, mask.length);
|
||||
vMyMask = vMyMask.replace('(', '');
|
||||
vMyMask = vMyMask.replace(')', '');
|
||||
switch (vMyMask) {
|
||||
case '#.##0':
|
||||
return (`(${addSeparators(vMyValue, '.', ',', 0)})`);
|
||||
case '#,##0':
|
||||
return (`(${addSeparators(vMyValue, ',', '.', 0)})`);
|
||||
case '-#.##0':
|
||||
return (`(${addSeparators(vMyValue, '.', ',', 0)})`);
|
||||
case '-#,##0':
|
||||
return (`(${addSeparators(vMyValue, ',', '.', 0)})`);
|
||||
default:
|
||||
return (`(${applyMask(vMyMask, vMyValue)})`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return (applyMask(mask, value));
|
||||
}
|
||||
}
|
||||
|
||||
function applyMask (originalMask, originalValue) {
|
||||
if (!originalMask || isNaN(Number(originalValue))) {
|
||||
return originalValue;
|
||||
}
|
||||
|
||||
let isNegative;
|
||||
let result;
|
||||
let integer;
|
||||
// find prefix/suffix
|
||||
let len = originalMask.length;
|
||||
const start = originalMask.search(/[0-9\-\+#]/);
|
||||
const prefix = start > 0 ? originalMask.substring(0, start) : '';
|
||||
// reverse string: not an ideal method if there are surrogate pairs
|
||||
let str = originalMask.split('')
|
||||
.reverse()
|
||||
.join('');
|
||||
const end = str.search(/[0-9\-\+#]/);
|
||||
let offset = len - end;
|
||||
const substr = originalMask.substring(offset, offset + 1);
|
||||
let index = offset + ((substr === '.' || (substr === ',')) ? 1 : 0);
|
||||
const suffix = end > 0 ? originalMask.substring(index, len) : '';
|
||||
|
||||
// mask with prefix & suffix removed
|
||||
let mask = originalMask.substring(start, index);
|
||||
|
||||
// convert any string to number according to formation sign.
|
||||
let value = mask.charAt(0) === '-' ? -originalValue : Number(originalValue);
|
||||
isNegative = value < 0 ? value = -value : 0; // process only abs(), and turn on flag.
|
||||
|
||||
// search for separator for grp & decimal, anything not digit, not +/- sign, not #.
|
||||
result = mask.match(/[^\d\-\+#]/g);
|
||||
const decimal = (result && result[result.length - 1]) || '.'; // treat the right most symbol as decimal
|
||||
const group = (result && result[1] && result[0]) || ','; // treat the left most symbol as group separator
|
||||
|
||||
// split the decimal for the format string if any.
|
||||
mask = mask.split(decimal);
|
||||
// Fix the decimal first, toFixed will auto fill trailing zero.
|
||||
value = value.toFixed(mask[1] && mask[1].length);
|
||||
value = String(Number(value)); // convert number to string to trim off *all* trailing decimal zero(es)
|
||||
|
||||
// fill back any trailing zero according to format
|
||||
const posTrailZero = mask[1] && mask[1].lastIndexOf('0'); // look for last zero in format
|
||||
const part = value.split('.');
|
||||
// integer will get !part[1]
|
||||
if (!part[1] || (part[1] && part[1].length <= posTrailZero)) {
|
||||
value = (Number(value)).toFixed(posTrailZero + 1);
|
||||
}
|
||||
const szSep = mask[0].split(group); // look for separator
|
||||
mask[0] = szSep.join(''); // join back without separator for counting the pos of any leading 0.
|
||||
|
||||
const posLeadZero = mask[0] && mask[0].indexOf('0');
|
||||
if (posLeadZero > -1) {
|
||||
while (part[0].length < (mask[0].length - posLeadZero)) {
|
||||
part[0] = `0${part[0]}`;
|
||||
}
|
||||
} else if (Number(part[0]) === 0) {
|
||||
part[0] = '';
|
||||
}
|
||||
|
||||
value = value.split('.');
|
||||
value[0] = part[0];
|
||||
|
||||
// process the first group separator from decimal (.) only, the rest ignore.
|
||||
// get the length of the last slice of split result.
|
||||
const posSeparator = (szSep[1] && szSep[szSep.length - 1].length);
|
||||
if (posSeparator) {
|
||||
integer = value[0];
|
||||
str = '';
|
||||
offset = integer.length % posSeparator;
|
||||
len = integer.length;
|
||||
for (index = 0; index < len; index++) {
|
||||
str += integer.charAt(index); // ie6 only support charAt for sz.
|
||||
// -posSeparator so that won't trail separator on full length
|
||||
// jshint -W018
|
||||
if (!((index - offset + 1) % posSeparator) && index < len - posSeparator) {
|
||||
str += group;
|
||||
}
|
||||
}
|
||||
value[0] = str;
|
||||
}
|
||||
value[1] = (mask[1] && value[1]) ? decimal + value[1] : '';
|
||||
|
||||
// remove negative sign if result is zero
|
||||
result = value.join('');
|
||||
if (result === '0' || result === '') {
|
||||
// remove negative sign if result is zero
|
||||
isNegative = false;
|
||||
}
|
||||
|
||||
// put back any negation, combine integer and fraction, and add back prefix & suffix
|
||||
return prefix + ((isNegative ? '-' : '') + result) + suffix;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import initializeStore from './store';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Root from './root.jsx';
|
||||
|
||||
export default async function paint ($element, layout, component) {
|
||||
const state = await initializeStore({
|
||||
$element,
|
||||
component,
|
||||
layout
|
||||
});
|
||||
const editmodeClass = component.inAnalysisState() ? '' : 'edit-mode';
|
||||
const jsx = (
|
||||
<Root
|
||||
qlik={component}
|
||||
state={state}
|
||||
editmodeClass={editmodeClass}
|
||||
/>
|
||||
);
|
||||
|
||||
ReactDOM.render(jsx, $element[0]);
|
||||
}
|
||||
167
src/root.jsx
167
src/root.jsx
@@ -1,60 +1,121 @@
|
||||
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';
|
||||
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";
|
||||
|
||||
const Root = ({ state, qlik, editmodeClass }) => (
|
||||
<LinkedScrollWrapper>
|
||||
<div className={`kpi-table ${editmodeClass}`}>
|
||||
<HeadersTable
|
||||
data={state.data}
|
||||
general={state.general}
|
||||
qlik={qlik}
|
||||
styling={state.styling}
|
||||
/>
|
||||
<LinkedScrollSection linkVertical>
|
||||
<DataTable
|
||||
data={state.data}
|
||||
general={state.general}
|
||||
qlik={qlik}
|
||||
renderData={false}
|
||||
styling={state.styling}
|
||||
/>
|
||||
</LinkedScrollSection>
|
||||
</div>
|
||||
<div className={`data-table ${editmodeClass}`}>
|
||||
<LinkedScrollSection linkHorizontal>
|
||||
<HeadersTable
|
||||
data={state.data}
|
||||
general={state.general}
|
||||
qlik={qlik}
|
||||
styling={state.styling}
|
||||
/>
|
||||
</LinkedScrollSection>
|
||||
<LinkedScrollSection
|
||||
linkHorizontal
|
||||
linkVertical
|
||||
>
|
||||
<DataTable
|
||||
data={state.data}
|
||||
general={state.general}
|
||||
qlik={qlik}
|
||||
styling={state.styling}
|
||||
/>
|
||||
</LinkedScrollSection>
|
||||
</div>
|
||||
</LinkedScrollWrapper>
|
||||
);
|
||||
class Root extends React.PureComponent {
|
||||
render () {
|
||||
const { editmodeClass, component, state } = this.props;
|
||||
const { data, general, styling, error, element } = state;
|
||||
|
||||
// Determine cell- and column separator width
|
||||
let cellWidth = '0px';
|
||||
let columnSeparatorWidth = '';
|
||||
if (!error && element) {
|
||||
const tableWidth = element.getBoundingClientRect().width;
|
||||
|
||||
if (general.cellWidth) {
|
||||
cellWidth = general.cellWidth;
|
||||
if (general.useColumnSeparator) {
|
||||
columnSeparatorWidth = '8px';
|
||||
}
|
||||
} else {
|
||||
// 230 is the left "header", rest is magic margins
|
||||
const headerMarginRight = 8 + 230 + 20;
|
||||
const borderWidth = 1;
|
||||
const rowCellCount = data.matrix[0].length;
|
||||
|
||||
let separatorCount = 0;
|
||||
let separatorWidth = 0;
|
||||
if (general.useColumnSeparator) {
|
||||
separatorCount = data.headers.dimension2.length - 1;
|
||||
separatorWidth = Math.min(
|
||||
Math.floor((tableWidth * 0.2) / separatorCount),
|
||||
8
|
||||
);
|
||||
columnSeparatorWidth = `${separatorWidth}px`;
|
||||
}
|
||||
|
||||
const separatorWidthSum =
|
||||
(separatorWidth + borderWidth) * separatorCount;
|
||||
cellWidth = `${Math.floor(
|
||||
(tableWidth - separatorWidthSum - headerMarginRight - borderWidth) /
|
||||
rowCellCount) - borderWidth}px`;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="root">
|
||||
{error ? (
|
||||
<div className={`error ${editmodeClass}`}>
|
||||
{state.layout.errormessage}
|
||||
</div>
|
||||
) : (
|
||||
<LinkedScrollWrapper>
|
||||
<div className={`kpi-table ${editmodeClass}`}>
|
||||
<HeadersTable
|
||||
cellWidth={cellWidth}
|
||||
columnSeparatorWidth={columnSeparatorWidth}
|
||||
component={component}
|
||||
data={data}
|
||||
general={general}
|
||||
isKpi
|
||||
styling={styling}
|
||||
/>
|
||||
<LinkedScrollSection linkVertical>
|
||||
<DataTable
|
||||
cellWidth={cellWidth}
|
||||
columnSeparatorWidth={columnSeparatorWidth}
|
||||
component={component}
|
||||
data={data}
|
||||
general={general}
|
||||
renderData={false}
|
||||
styling={styling}
|
||||
/>
|
||||
</LinkedScrollSection>
|
||||
</div>
|
||||
<div
|
||||
className={`data-table ${editmodeClass}`}
|
||||
style={{ width: general.cellWidth ? 'auto' : '100%' }}
|
||||
>
|
||||
<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 = {
|
||||
qlik: PropTypes.shape({}).isRequired,
|
||||
component: PropTypes.shape({}).isRequired,
|
||||
editmodeClass: PropTypes.string.isRequired,
|
||||
state: PropTypes.shape({
|
||||
data: PropTypes.object.isRequired,
|
||||
general: PropTypes.object.isRequired,
|
||||
styling: PropTypes.object.isRequired
|
||||
}).isRequired,
|
||||
editmodeClass: PropTypes.string.isRequired
|
||||
data: PropTypes.object,
|
||||
general: PropTypes.object,
|
||||
styling: PropTypes.object
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
export default Root;
|
||||
|
||||
@@ -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`; },
|
||||
|
||||
@@ -2,13 +2,15 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const handleCalculateTooltipPosition = (event) => {
|
||||
const tooltipClassName = 'tooltip-wrapper';
|
||||
const tooltip = document.getElementsByClassName(tooltipClassName);
|
||||
|
||||
tooltip[0].style.left = event.clientX + 'px';
|
||||
tooltip[0].style.top = event.clientY + 'px';
|
||||
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.Component {
|
||||
|
||||
class Tooltip extends React.PureComponent {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
@@ -17,38 +19,27 @@ class Tooltip extends React.Component {
|
||||
this.handleRenderTooltip = this.handleRenderTooltip.bind(this);
|
||||
}
|
||||
|
||||
shouldComponentUpdate (nextProps, nextState) {
|
||||
const { showTooltip } = this.state;
|
||||
if (nextState.showTooltip === showTooltip) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
handleRenderTooltip () {
|
||||
const { showTooltip } = this.state;
|
||||
this.setState({ showTooltip: !showTooltip });
|
||||
}
|
||||
|
||||
|
||||
render () {
|
||||
const { children, tooltipText } = this.props;
|
||||
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>
|
||||
<div className="tooltip-wrapper">
|
||||
<p style={{ fontFamily: styling.options.fontFamily }}>
|
||||
{tooltipText}
|
||||
</p>
|
||||
</div>
|
||||
@@ -59,7 +50,16 @@ class Tooltip extends React.Component {
|
||||
}
|
||||
|
||||
Tooltip.propTypes = {
|
||||
children: PropTypes.string.isRequired,
|
||||
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,21 +9,6 @@ export function distinctArray (array) {
|
||||
.map(entry => JSON.parse(entry));
|
||||
}
|
||||
|
||||
export function addSeparators (number, thousandSeparator, decimalSeparator, numberOfDecimals) {
|
||||
const numberString = number.toFixed(numberOfDecimals);
|
||||
const numberStringParts = numberString.split('.');
|
||||
let [
|
||||
wholeNumber,
|
||||
decimal
|
||||
] = numberStringParts;
|
||||
decimal = numberStringParts.length > 1 ? decimalSeparator + decimal : '';
|
||||
const regexCheckForThousand = /(\d+)(\d{3})/;
|
||||
while (regexCheckForThousand.test(wholeNumber)) {
|
||||
wholeNumber = wholeNumber.replace(regexCheckForThousand, `$1${thousandSeparator}$2`);
|
||||
}
|
||||
return wholeNumber + decimal;
|
||||
}
|
||||
|
||||
export function Deferred () {
|
||||
this.promise = new Promise((resolve, reject) => {
|
||||
this.resolve = resolve;
|
||||
@@ -31,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 }
|
||||
@@ -41,7 +26,7 @@ export function injectSeparators (array, shouldHaveSeparator, suppliedOptions) {
|
||||
...suppliedOptions
|
||||
};
|
||||
|
||||
if (!shouldHaveSeparator) {
|
||||
if (!columnSeparatorWidth) {
|
||||
return array;
|
||||
}
|
||||
return array.reduce((result, entry, index) => {
|
||||
|
||||
@@ -84,7 +84,7 @@ module.exports = {
|
||||
'indentation': 2,
|
||||
'length-zero-no-unit': true,
|
||||
'max-empty-lines': 1,
|
||||
'max-nesting-depth': 3,
|
||||
'max-nesting-depth': 5,
|
||||
'media-feature-colon-space-after': 'always',
|
||||
'media-feature-colon-space-before': 'never',
|
||||
'media-feature-name-case': 'lower',
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,10 +0,0 @@
|
||||
import Enzyme from 'enzyme';
|
||||
import Adapter from 'enzyme-adapter-react-16';
|
||||
import jasmineEnzyme from 'jasmine-enzyme';
|
||||
Enzyme.configure({ adapter: new Adapter() });
|
||||
|
||||
beforeEach(() => {
|
||||
jasmineEnzyme();
|
||||
});
|
||||
|
||||
export * from 'enzyme';
|
||||
@@ -1,9 +0,0 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'test-utilities/enzyme-setup';
|
||||
|
||||
export function mountedComponent (Model, Component, props = {}) {
|
||||
const component = mount((
|
||||
<Component {...props} />
|
||||
)).find(Component);
|
||||
return new Model(component);
|
||||
}
|
||||
@@ -1,11 +1,17 @@
|
||||
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");
|
||||
// eslint-disable-next-line no-process-env
|
||||
const MODE = process.env.NODE_ENV || 'development';
|
||||
const SOURCE_MAP = 'source-map';
|
||||
const DEVTOOL = MODE === 'development' ? SOURCE_MAP : false;
|
||||
|
||||
console.log('Webpack mode:', MODE); // eslint-disable-line no-console
|
||||
|
||||
const config = {
|
||||
devtool: 'source-map',
|
||||
devtool: DEVTOOL,
|
||||
entry: ['./src/index.js'],
|
||||
externals: {
|
||||
jquery: {
|
||||
@@ -13,13 +19,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,22 +67,11 @@ 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({
|
||||
files: '**/*.less'
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user