Compare commits

...

106 Commits

Author SHA1 Message Date
Purwa Shrivastava
8c093a4692 Merge pull request #86 from qlik-oss/QLIK-98564/sourceMaps
Removing source maps from production mode.
2020-02-14 11:03:05 +01:00
Avinash
ec822b843b Merge pull request #85 from qlik-oss/bugfix/QB-959
bugfix for  Column separator is misaligned
2020-02-14 11:51:42 +05:30
Purwa Shrivastava
5dc8bb49a4 Removing souce maps from production mode. 2020-02-12 08:06:07 +01:00
Avinash Shinde
0d98553a71 fix: removing .lock file 2020-02-11 16:32:58 +05:30
Avinash Shinde
6513a294b3 fix: Column separator is misaligned 2020-02-10 15:32:43 +05:30
Shiben Dutta
79e753c2b2 Merge pull request #84 from qlik-oss/QB262
fix: disable data export as per QMC
2020-02-04 08:26:39 +05:30
Shiben Dutta
468598540f fix: disable data export as per QMC 2020-01-16 17:22:50 +05:30
snigdhaprasad26
045d0db215 Merge pull request #83 from qlik-oss/QB-285-console-error
console error fixed
2020-01-10 13:43:25 +05:30
Snigdha Snigdha
73011d0388 fix: eslint error fixed and yarn.lock file removed 2020-01-09 16:24:15 +05:30
Snigdha Snigdha
41cf77e8d2 fix: console error fixed of qb-285 2020-01-09 15:49:47 +05:30
Snigdha Snigdha
1355381346 fix: console error fixed of qb-285 2020-01-09 15:18:05 +05:30
Snigdha Snigdha
730f35a83c fix: console error fixed of qb-285 2020-01-09 15:11:11 +05:30
Snigdha Snigdha
71bf25e8fb fix: console error fixed of qb-285 2020-01-09 15:08:57 +05:30
Snigdha Snigdha
9b4fe54239 fix: console error of qb-285 2020-01-09 14:56:56 +05:30
Snigdha Snigdha
e59c594215 console error fixed 2019-12-31 19:44:54 +05:30
Purwa Shrivastava
97a54e6f5a Merge pull request #82 from qlik-oss/qb-612/missingvaluesfix
Fix missing values resulting in data being rendered in wrong columns.
2019-12-10 14:40:45 +01:00
Purwa Shrivastava
eac9fd2a5f Fix missing values resulting in data being rendered in wrong columns. 2019-12-10 12:29:28 +01:00
Purwa Shrivastava
96f09f9323 Merge pull request #81 from qlik-oss/QB-377/errorMessage
React rendering Errors for few apps.
2019-12-09 08:42:31 +01:00
Purwa Shrivastava
2ab340f3f1 React rendering Errors for few apps. 2019-12-06 11:11:25 +01:00
Purwa Shrivastava
58d0f542eb Merge pull request #80 from qlik-oss/QB-377/errorMessage
Corrected the react error when componentDidUpdate is called. So the e…
2019-12-06 10:15:05 +01:00
Purwa Shrivastava
8ba826a0ea Corrected the react error when componentDidUpdate is called. So the error message is uniformly shown in both analysis and edit mode. 2019-12-06 10:12:07 +01:00
Purwa Shrivastava
d2446395e2 Merge pull request #77 from qlik-oss/QB-331/PivotPagination
Pivot Pagination Fix.
2019-12-05 13:41:29 +01:00
Purwa Shrivastava
f17a7b7714 Merge pull request #79 from qlik-oss/QB-493/wrongDataDisplay
Qb 493/wrong data display
2019-12-05 09:15:42 +01:00
Purwa Shrivastava
8aa86275f0 Changed the rendering of each data cell to check for the headers of each dimesnsion and measure where it shall be written. 2019-12-04 11:15:14 +01:00
Purwa Shrivastava
3ebc2b9e29 Adding Dim 2 to the rendering logic. 2019-11-29 16:17:49 +01:00
Purwa Shrivastava
ce81549011 Merge pull request #78 from qlik-oss/QB-498/disableConversion
Conversion led to different sort orders of data in different objects …
2019-11-28 13:40:45 +01:00
Purwa Shrivastava
e64af66dab Conversion led to different sort orders of data in different objects so disabling it. 2019-11-27 14:54:42 +01:00
Purwa Shrivastava
fd9e645fc1 Pivot Pagination Fix. 2019-11-21 10:59:28 +01:00
Philip Olsén
ca1b1a9b53 Merge pull request #75 from qlik-oss/pol/bd
Update black duck link
2019-09-20 15:33:56 +02:00
Philip Olsén
a65f843008 Update black duck link 2019-09-20 14:23:37 +02:00
Purwa Shrivastava
be9570e0aa Merge pull request #74 from qlik-oss/atq/about
Updated the About info for the smart pivot.
2019-07-12 12:40:29 +02:00
Purwa Shrivastava
41d3a7c9af Updated the About info for the smart pivot. 2019-07-11 11:01:42 +02:00
Albert Backenhof
1e0a7c1204 Merge pull request #72 from qlik-oss/DEB-211/ContextMenuExportExcel
Moved Excel export to context menu
2019-06-27 12:35:04 +02:00
Albert Backenhof
17b5df296c Moved Excel export to context menu
-Not showing context menu option for desktop
 users.

Issue: DEB-211
2019-06-26 13:41:07 +02:00
Albert Backenhof
64b778b1af Merge pull request #71 from qlik-oss/DEB-244/ExcelRow
Suppress missing values and use correct cube size
2019-06-17 13:37:05 +02:00
Albert Backenhof
be6718ccf4 Merge pull request #73 from qlik-oss/DEB-250/ImprovedSelection
Selecting with backendApi instead of Field
2019-06-17 09:30:27 +02:00
Albert Backenhof
28e6237a43 Selecting with backendApi instead of Field
-Makes it possible to select master fields with
 different names than the original field.

-Alternate state is no longer a problem when using
 this technique to select.

Issue: DEB-250
2019-06-14 08:30:57 +02:00
Albert Backenhof
4fab3b5933 Suppress missing values and use correct cube size
Issue: DEB-244
2019-05-29 15:17:17 +02:00
Albert Backenhof
79339a8963 Merge pull request #70 from qlik-oss/QLIK-95962/SpanWidth
Add option to fit cells to chart width
2019-05-29 12:19:59 +02:00
Albert Backenhof
a6faeeb656 Add option to fit cells to chart width
-To achieve pixel perfection the container of both
 header and data needs to calculate the size of the
 child tables' cells.

Issue: QLIK-95962
2019-05-28 07:44:40 +02:00
Albert Backenhof
c22b7f5c6b Merge pull request #61 from qlik-oss/DEB-232/DoubleMeasure
Made the table cell keys more unique
2019-05-27 14:37:07 +02:00
Albert Backenhof
80f97602e4 Merge pull request #69 from qlik-oss/DEB-242/ExportInContainer
Excel export inside container
2019-05-27 14:36:31 +02:00
Albert Backenhof
f745656b4c Excel export inside container
-No tid-element available when inside native container.
 Therefore, use root element to find table cells instead.

Issue: DEB-242
2019-05-27 14:33:43 +02:00
Albert Backenhof
f7e780b92e Merge pull request #68 from qlik-oss/excelType
Specifying excel type of excel download
2019-05-27 14:28:26 +02:00
Albert Backenhof
d7a76c7db9 Merge pull request #63 from qlik-oss/QLIK-95802/AltStateFix
Fixed alternate state support
2019-05-27 14:26:58 +02:00
Albert Backenhof
16c380e1c6 Fixed alternate state support
-Previously, the shown data always followed
 default state, and not the set state.
-Previously, the selections made in smart pivot
 sometimes used the previous alternate state.

Issue: QLIK-95802
2019-05-27 14:24:02 +02:00
Albert Backenhof
072a3b80c4 Merge pull request #62 from qlik-oss/DEB-233/Selections
Handle selection toggle properly
2019-05-27 14:12:43 +02:00
Albert Backenhof
5572b7ce67 Merge pull request #64 from qlik-oss/DEB-221/WidthSlider
Slider for cell width without presets
2019-05-27 14:11:01 +02:00
Albert Backenhof
71cf92c217 Slider for cell width without presets
-Previous slider only has three presets, this new
 solution sets the actual width.

Issue: DEB-221
2019-05-27 14:08:25 +02:00
Albert Backenhof
e26d5fded8 Specifying excel type of excel download 2019-05-20 10:12:22 +02:00
Albert Backenhof
20282b0b99 Merge pull request #65 from qlik-oss/QLIK-95907/QlikFormatting
Using qlik formatting measurements
2019-05-20 10:05:48 +02:00
Albert Backenhof
5f18321ccf Merge pull request #67 from qlik-oss/DEB-136/readme
Updated github readme
2019-05-20 09:07:54 +02:00
Albert Backenhof
d14f5951ac Using qlik formatting measurements
Issue: QLIK-95907
2019-05-20 08:02:15 +02:00
Albert Backenhof
5872ee7b58 Updated github readme
Issue: DEB-136
2019-05-20 07:02:45 +02:00
Albert Backenhof
1436463f59 Made the table cell keys more unique
-This makes it possible to use the same measurement
 multiple times within the same smart-pivot chart.

Issue: DEB-232
2019-05-13 13:12:32 +02:00
Albert Backenhof
729a31920d Handle selection toggle properly
-Previously, the cell selection would toggle
 the current selection. This meant, if a column
 is already selected when making a cell selection,
 the column selection would toggle off. With this
 fix the column selection stays on.

Issue: DEB-233
2019-05-13 09:43:41 +02:00
Purwa Shrivastava
6d305b21b2 Merge pull request #60 from qlik-oss/revert-59-DEB-217/CellFormatting
Revert "Fixed cell number formatting"
2019-05-10 12:48:04 +02:00
Purwa Shrivastava
0b210e0d35 Revert "Fixed cell number formatting" 2019-05-10 12:44:32 +02:00
Albert Backenhof
c8afb83130 Merge pull request #59 from qlik-oss/DEB-217/CellFormatting
Fixed cell number formatting
2019-05-10 10:19:47 +02:00
Albert Backenhof
49981f6ae3 Fixed cell number formatting
-Now will always use the built in Qlik value format.

Issue: DEB-217, DEB-218, DEB-219, DEB-220
2019-05-09 10:09:33 +02:00
Albert Backenhof
b3b17e8d0c Merge pull request #58 from qlik-oss/DEB-173/MasterObject
Fixed master object bug
2019-05-03 09:28:27 +02:00
Albert Backenhof
5d45f57e00 Fixed master object bug
-Previously the smart pivot didn't render as
 a master object.

Issue: DEB-173
2019-05-03 09:09:44 +02:00
Albert Backenhof
2f59d97cf3 Merge pull request #55 from qlik-oss/DEB-204/NewString
Updated string according to Ralf Narfeldt
2019-05-03 08:26:36 +02:00
Albert Backenhof
b8d6b0a53e Merge pull request #53 from qlik-oss/QLIK-95019/Excel
Improved excel and snapshot export
2019-05-03 08:26:25 +02:00
Purwa Shrivastava
6cc82e7b38 Merge pull request #57 from qlik-oss/DEB-213/misalignedTable
DEB-213: Mis calculated the default, changed from 23 to 25 px.
2019-05-02 14:25:28 +02:00
Purwa Shrivastava
cc7e3e62ed DEB-213: Mis calculated the default, changed from 23 to 25 px. 2019-05-02 14:19:36 +02:00
Purwa Shrivastava
26de3b63ed Merge pull request #56 from qlik-oss/DEB-213/misalignedTable
DEB-213: Introdued a minimum height for the table rows, so even if th…
2019-05-02 13:04:17 +02:00
Purwa Shrivastava
8eafeffcec DEB-213: Introdued a minimum height for the table rows, so even if the content is null, the rows are displayed even. 2019-05-02 11:31:15 +02:00
Albert Backenhof
ef7926dd13 Updated string according to Ralf Narfeldt
Issue: DEB-204
2019-04-30 14:51:30 +02:00
Albert Backenhof
af307fd24b Improved excel and snapshot export
-KPI is now included in excel.
-Snapshots now work.

Issue: QLIK-95019
2019-04-30 13:04:42 +02:00
Albert Backenhof
98401db922 Merge pull request #52 from qlik-oss/saturateMatrix
Fixed bug that prevented row saturation
2019-04-23 07:01:34 +02:00
Albert Backenhof
75771e4815 Merge pull request #50 from qlik-oss/DEB-187/ColorMeasures
Color by measure indices
2019-04-23 07:01:25 +02:00
Albert Backenhof
a12205c840 Fixed bug that prevented row saturation 2019-04-18 09:44:07 +02:00
Albert Backenhof
ebfee69c7b Merge pull request #51 from qlik-oss/DEB-188/LimitSize
Fixed layout and styling of table
2019-04-18 08:28:26 +02:00
Albert Backenhof
05c25c72cb Fixed layout and styling of table
-Now using flexbox to make the layout more
 dynamic.
-Row selection of hover works as intended.
-Improved the way scrollbars are shown.

Issue: DEB-188, DEB-192
2019-04-17 11:02:23 +02:00
Albert Backenhof
d2a24dd256 Color by measure indices
-Used to specify what measure columns should
 use conditional coloring.

Issue: DEB-187
2019-04-15 14:15:12 +02:00
Albert Backenhof
a6a6ef24b5 Merge pull request #49 from qlik-oss/DEB-183/CellSpacing
Fixed cellspacing on IE, Edge and FF
2019-04-15 07:01:48 +02:00
Albert Backenhof
8374ecce85 Merge pull request #48 from qlik-oss/DEB-182/OverlayScrollbar
Overlay scrollbar in webkit browsers
2019-04-15 07:01:29 +02:00
Albert Backenhof
0135d4fc64 Merge pull request #43 from qlik-oss/DEB-176/EmptyMeasureCells
Show cells for single-dimension tables
2019-04-15 07:01:06 +02:00
Albert Backenhof
47d7f33cb9 Merge pull request #45 from qlik-oss/DEB-168/MissingText
If dimension value doesn't contain qText use qNum
2019-04-15 07:00:46 +02:00
Albert Backenhof
4c2e483592 Merge pull request #47 from qlik-oss/DEB-153/Icon
Updated the icon to pivot-table
2019-04-12 14:24:54 +02:00
Albert Backenhof
801c7c862e Merge pull request #46 from qlik-oss/DEB-177/ConditionalColoring
Replaced semaphores with Conditional Coloring
2019-04-12 10:56:05 +02:00
Albert Backenhof
f3bda74202 Merge pull request #44 from qlik-oss/DEB-164/FontIssue
Improved header arrangement
2019-04-12 09:43:44 +02:00
Albert Backenhof
3ec5d13ae6 Fixed cellspacing on IE, Edge and FF
Issue: DEB-183
2019-04-12 09:37:49 +02:00
Albert Backenhof
2133bb44ef Overlay scrollbar in webkit browsers
-To prevent scrollbar from offsetting the cells
 those browsers that support overlay scrollbars
 should use it.

Issue: DEB-182
2019-04-12 08:15:37 +02:00
Albert Backenhof
e447666982 Merge pull request #40 from qlik-oss/DEB-160/ErrMsg
Updated default error message
2019-04-12 07:06:57 +02:00
Albert Backenhof
7038140088 Updated the icon to pivot-table
Issue: DEB-153
2019-04-11 14:48:42 +02:00
Albert Backenhof
b5d9633496 Replaced semaphores with Conditional Coloring
-Was unclear what the two semaphores did and meant.
 Also, Concept Semaphores didn't seem to work.

Issue: DEB-177
2019-04-11 14:40:06 +02:00
Albert Backenhof
a518432db4 If dimension value doesn't contain qText use qNum
Issue: DEB-168
2019-04-11 10:31:47 +02:00
Albert Backenhof
f1875702be Improved header arrangement
-Only add elements that are displayed.
-Properly align content.
-Use correct font.

Issue: DEB-163, DEB-164
2019-04-11 09:04:53 +02:00
Albert Backenhof
b1a9130663 Show cells for single-dimension tables
-Recent commit introduced a bug that prevented
 single-dimension tables from showing any cells.

Issue: DEB-176
2019-04-10 13:41:24 +02:00
Albert Backenhof
dfb30285ab Updated default error message
-Previous isn't grammatically correct.

Issue: DEB-160
2019-04-10 12:15:28 +02:00
Albert Backenhof
c8cd78ae5f Merge pull request #37 from qlik-oss/DEB-156/OnlyTwoDims
Moved design dimension to Appearance->Table format
2019-04-10 12:14:20 +02:00
Albert Backenhof
a5bc3ecd1b Moved design dimension to Appearance->Table format
-According to the documentation it should only be
 possible to set two dimensions. Previously you
 were able to set three, where the third one was
 used for styling. This wasn't obvious to the user
 though. Now, the design field is set under
 Appearance -> Table format.

Issue: DEB-156
2019-04-10 12:12:08 +02:00
Albert Backenhof
bec68e7cd4 Merge pull request #39 from qlik-oss/DEB-159/Props
Changed order of properties and added About
2019-04-10 12:05:15 +02:00
Albert Backenhof
02bba4ad5a Merge pull request #38 from qlik-oss/DEB-157/NineMeasures
1 Dim and 9 Measures OR 2 Dim and 8 Measures
2019-04-10 11:30:44 +02:00
Albert Backenhof
57c4b12b24 Merge pull request #36 from qlik-oss/DEB-154/EmptyData
Improved setup of data matrix
2019-04-10 09:13:34 +02:00
Albert Backenhof
ca33540fd6 Merge pull request #35 from qlik-oss/DEB-152/HeaderWidth
The header width should match the table format
2019-04-10 09:13:03 +02:00
Albert Backenhof
cac3fabb2f Changed order of properties and added About
Issue: DEB-159
2019-04-10 08:13:24 +02:00
Albert Backenhof
c57b0edea8 1 Dim and 9 Measures OR 2 Dim and 8 Measures
-Dynamic max values for Dimensions and Measures.

Issue: DEB-157
2019-04-10 07:50:07 +02:00
Albert Backenhof
3862bd294c Arrange matching dim1 data on the same row
-The data is not guaranteed to be processed row
 by row. Therefore, make sure to check the entire
 matrix for the correct row (not just previous row)
 when appending new row data.

Issue: DEB-155
2019-04-09 08:29:37 +02:00
Albert Backenhof
0c18523891 Empty data should result in emtpy cells
-Previously, empty data resulted in the cells
 completely missing. This caused other cells
 to not end up in the correct column.

Issue: DEB-154
2019-04-09 07:50:33 +02:00
Albert Backenhof
629821bd6b The header width should match the table format
Issue: DEB-152
2019-04-08 10:39:02 +02:00
Albert Backenhof
08c5cf8104 Merge pull request #34 from qlik-oss/DEB-130/VersionInDesc
Aligned build to how Dashboard bundle extensions
2019-04-05 14:40:13 +02:00
Tobias Åström
d3c39bea75 Merge pull request #32 from qlik-oss/tsm-color-picker-check
Update metric-semaphores.js
2019-04-05 13:46:58 +02:00
Tobias Åström
34527c3d6d Update metric-semaphores.js
We recently had a bug on the color picker when used in extensions (QLIK-94131) so I went through all extensions to check their status, this was the only one with a potential problem.
The property notation should be as I changed it now (but after fixing the bug it will work anyway). However, there might be code elsewhere in the extension that mitigates the bug, so make sure this gets tested.
2019-03-12 09:04:43 +01:00
34 changed files with 1258 additions and 1339 deletions

View File

@@ -20,7 +20,7 @@ jobs:
command: npm install
- run:
name: BlackDuck scan
command: curl -s https://blackducksoftware.github.io/hub-detect/hub-detect.sh | bash -s -- \
command: curl -s https://detect.synopsys.com/detect.sh | bash -s -- \
--blackduck.url="https://qliktech.blackducksoftware.com" \
--blackduck.trust.cert=true \
--blackduck.username="svc-blackduck" \

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"json.format.enable": false
}

View File

@@ -1,52 +1,22 @@
# P&L Smart Pivot, a Qlik Sense Extension for Financial reporting
[![CircleCI](https://circleci.com/gh/qlik-oss/PLSmartPivot.svg?style=svg)](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).
# 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: 2.2 KiB

View File

@@ -15,7 +15,7 @@ gulp.task('qext', function () {
type: 'visualization',
description: pkg.description + '\nVersion: ' + VERSION,
version: VERSION,
icon: 'table',
icon: 'pivot-table',
preview: 'qlik-smart-pivot.png',
keywords: 'qlik-sense, visualization',
author: pkg.author,

View File

@@ -1,88 +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';
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 = (/%/).test(measurement.format);
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 dimension1Row = measurement.parents.dimension1.elementNumber;
const isSpecifiedMetricField = fieldsToApplyTo.metricsSpecificFields.indexOf(dimension1Row) !== -1;
const shouldHaveSemaphoreColors = (fieldsToApplyTo.applyToMetric || isSpecifiedMetricField);
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}
@@ -93,22 +97,24 @@ 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: function (props, propName) {
selectValues (props, propName) {
if (props.isSnapshot || typeof props[propName] === 'function') {
return null;
}
@@ -126,60 +132,20 @@ DataCell.propTypes = {
export default DataCell;
function formatMeasurementValue (measurement, styling) {
const isColumnPercentageBased = (/%/).test(measurement.format);
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;
}
}
function formatMeasurementValue(measurement, styling) {
if (isNaN(measurement.value)) {
return styling.symbolForNulls;
}
return formattedMeasurementValue;
return measurement.displayValue;
}
function getSemaphoreColors (measurement, semaphoreColors) {
if (measurement.value < semaphoreColors.status.critical) {
return semaphoreColors.statusColors.critical;
function getConditionalColor(measurement, conditionalColoring) {
if (measurement.value < conditionalColoring.threshold.poor) {
return conditionalColoring.colors.poor;
}
if (measurement.value < semaphoreColors.status.medium) {
return semaphoreColors.statusColors.medium;
if (measurement.value < conditionalColoring.threshold.fair) {
return conditionalColoring.colors.fair;
}
return semaphoreColors.statusColors.normal;
return conditionalColoring.colors.good;
}

View File

@@ -5,98 +5,177 @@ 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}>
{!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
const { dimension1: dimension1Info, dimension2, measurement } = measurementData.parents;
const id = `${dimension1Info.elementNumber}-${dimension2 && dimension2.elementNumber}-${measurement.header}-${measurement.index}`;
return (
<DataCell
cellWidth={cellWidth}
component={component}
data={data}
general={general}
key={`${dimensionEntry.displayValue}-${id}`}
measurement={measurementData}
styleBuilder={styleBuilder}
styling={styling}
/>
);
})}
</tr>
);
})}
</tbody>
</table>
</div>
);
}
}
DataTable.defaultProps = {
renderData: true
};
DataTable.propTypes = {
cellWidth: PropTypes.string.isRequired,
columnSeparatorWidth: PropTypes.string.isRequired,
component: PropTypes.shape({}).isRequired,
data: PropTypes.shape({
headers: PropTypes.shape({
dimension1: PropTypes.array.isRequired
@@ -104,7 +183,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

View File

@@ -10,14 +10,15 @@ 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, qlik } = this.props;
const inEditState = qlik.inEditState();
const { entry, rowStyle, styleBuilder, styling, component } = this.props;
const inEditState = component.inEditState();
return (
<td
@@ -27,6 +28,7 @@ class RowHeader extends React.PureComponent {
>
<Tooltip
isTooltipActive={!inEditState}
styling={styling}
tooltipText={entry.displayValue}
>
<HeaderPadding
@@ -41,12 +43,9 @@ class RowHeader extends React.PureComponent {
}
RowHeader.propTypes = {
entry: PropTypes.shape({
displayValue: PropTypes.string.isRequired
}).isRequired,
qlik: PropTypes.shape({
component: PropTypes.shape({
backendApi: PropTypes.shape({
selectValues: function (props, propName) {
selectValues (props, propName) {
if (props.isSnapshot || typeof props[propName] === 'function') {
return null;
}
@@ -54,6 +53,10 @@ RowHeader.propTypes = {
}
}).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

View File

@@ -6,86 +6,86 @@ function createCube (definition, app) {
});
}
async function buildDataCube (originCubeDefinition, dimensionIndexes, app) {
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: 10
qWidth: originCube.qSize.qcx
}
],
qDimensions: [originCubeDefinition.qDimensions[dimensionIndexes.dimension1]],
qDimensions: [originCubeDefinition.qDimensions[0]],
qMeasures: originCubeDefinition.qMeasures
};
if (dimensionIndexes.dimension2) {
cubeDefinition.qDimensions.push(originCubeDefinition.qDimensions[dimensionIndexes.dimension2]);
if (originCube.qDimensionInfo.length === 2) {
cubeDefinition.qDimensions.push(originCubeDefinition.qDimensions[1]);
}
const cube = await createCube(cubeDefinition, app);
return cube.qHyperCube.qDataPages[0].qMatrix;
const cubeMatrix = cube.qHyperCube.qDataPages[0].qMatrix;
app.destroySessionObject(cube.qInfo.qId);
return cubeMatrix;
}
async function buildDesignCube (originCubeDefinition, dimensionIndexes, app) {
if (!dimensionIndexes.design) {
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;
}
const cube = await createCube({
qInitialDataFetch: [
{
qHeight: 1000,
qWidth: 1
}
],
qDimensions: [originCubeDefinition.qDimensions[dimensionIndexes.design]]
}, app);
return cube.qHyperCube.qDataPages[0].qMatrix;
}
const STYLE_SEPARATOR_COUNT = 7;
function findDesignDimension (qMatrix) {
return qMatrix[0].map(entry => (entry.qText.match(/;/g) || []).length).indexOf(STYLE_SEPARATOR_COUNT);
}
function getDimensionIndexes (dimensionsInformation, designDimensionIndex) {
const hasDesign = designDimensionIndex !== -1;
const nonDesignDimensionCount = hasDesign ? dimensionsInformation.length - 1 : dimensionsInformation.length;
const dimension1 = designDimensionIndex === 0 ? 1 : 0;
let dimension2 = false;
if (nonDesignDimensionCount === 2) {
dimension2 = hasDesign && designDimensionIndex < 2 ? 2 : 1;
}
const design = hasDesign && designDimensionIndex;
const firstMeasurementIndex = dimensionsInformation.length;
return {
design,
dimension1,
dimension2,
firstMeasurementIndex
};
}
export async function initializeCubes ({ component, layout }) {
const app = qlik.currApp(component);
const designDimensionIndex = findDesignDimension(layout.qHyperCube.qDataPages[0].qMatrix);
const dimensionsInformation = layout.qHyperCube.qDimensionInfo;
const dimensionIndexes = getDimensionIndexes(dimensionsInformation, designDimensionIndex);
let properties;
if (component.backendApi.isSnapshot) {
// Fetch properties of source
properties = (await app.getObjectProperties(layout.sourceObjectId)).properties;
} else {
properties = await component.backendApi.getProperties();
}
const originCubeDefinition = properties.qHyperCubeDef;
const designCube = await buildDesignCube(originCubeDefinition, dimensionIndexes, app);
const dataCube = await buildDataCube(originCubeDefinition, dimensionIndexes, app);
return {
design: designCube,
data: dataCube
};
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();
});
}

View File

@@ -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;

View 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;

View File

@@ -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: [
@@ -29,14 +29,14 @@ const header = {
index: 6,
color: '#4477aa'
},
label: 'Background Header Color',
label: 'Background color',
ref: 'HeaderColorSchema',
type: 'object',
dualOutput: true
},
HeaderTextColor: {
ref: 'HeaderTextColorSchema',
label: 'Text Header Color',
label: 'Text color',
component: 'color-picker',
defaultValue: {
index: 1,
@@ -47,7 +47,7 @@ const header = {
},
HeaderFontSize: {
ref: 'lettersizeheader',
translation: 'Font Size',
translation: 'Font size',
type: 'number',
component: 'buttongroup',
options: [

View File

@@ -1,8 +1,7 @@
import pagination from './pagination';
import header from './header';
import tableFormat from './table-format';
import conceptSemaphores from './concept-semaphores';
import metricSemaphores from './metric-semaphores';
import conditionalColoring from './conditional-coloring';
const definition = {
component: 'accordion',
@@ -18,18 +17,37 @@ const definition = {
},
uses: 'data'
},
sorting: {
uses: 'sorting'
},
settings: {
items: {
ConceptSemaphores: conceptSemaphores,
Formatted: tableFormat,
Header: header,
MetricSemaphores: metricSemaphores,
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'

View File

@@ -1,106 +0,0 @@
const metricSemaphores = {
type: 'items',
label: 'Metric Semaphores',
items: {
AllMetrics: {
ref: 'allmetrics',
type: 'boolean',
component: 'switch',
label: 'All metrics affected',
options: [
{
value: true,
label: 'On'
},
{
value: false,
label: 'Off'
}
],
defaultValue: false
},
MetricsAffected: {
ref: 'metricssemaphore',
translation: 'Metrics affected (1,2,4,...)',
type: 'string',
defaultValue: '-',
show (data) {
return !data.allmetrics;
}
},
MetricStatus1: {
ref: 'metricsstatus1',
translation: 'Critic is less than',
type: 'number',
defaultValue: -0.1
},
ColorStatus1: {
ref: 'colorstatus1',
label: 'Critic Color Fill',
type: 'object',
component: 'color-picker',
defaultValue: {
index: 8,
color: '#f93f17'
}
},
ColorStatus1Text: {
ref: 'colorstatus1text',
label: 'Critic Color Text',
type: 'object',
component: 'color-picker',
defaultValue: {
index: 11,
color: '#ffffff'
}
},
MetricStatus2: {
ref: 'metricsstatus2',
translation: 'Medium is less than',
type: 'number',
defaultValue: 0
},
ColorStatus2: {
ref: 'colorstatus2',
label: 'Medium Color Fill',
type: 'object',
component: 'color-picker',
defaultValue: {
index: 9,
color: '#ffcf02'
}
},
ColorStatus2Text: {
ref: 'colorstatus2text',
label: 'Medium Color Text',
type: 'object',
component: 'color-picker',
defaultValue: {
index: 12,
color: '#000000'
}
},
ColorStatus3: {
ref: 'colorstatus3',
label: 'Success Color Fill',
type: 'object',
component: 'color-picker',
defaultValue: {
index: 10,
color: '#276e27'
}
},
ColorStatus3Text: {
ref: 'colorstatus3text',
label: 'Success Color Text',
type: 'object',
component: 'color-picker',
defaultValue: {
index: 11,
color: '#ffffff'
}
}
}
};
export default metricSemaphores;

View File

@@ -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.`
}
}
};

View File

@@ -1,12 +1,53 @@
const qlik = window.require('qlik');
// fixes case for when there are 3 dimensions, missies the case with 1 design dimension and 1 data dimension
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',
@@ -17,7 +58,7 @@ const tableFormat = {
SeparatorColumns: {
ref: 'separatorcols',
type: 'boolean',
label: 'Separator Columns',
label: 'Column separators',
defaultValue: false
},
rowEvenBGColor: {
@@ -48,7 +89,7 @@ const tableFormat = {
ref: 'BodyTextColorSchema',
type: 'string',
component: 'dropdown',
label: 'Text Body Color',
label: 'Text body color',
options: [
{
value: 'Black',
@@ -98,7 +139,7 @@ const tableFormat = {
ref: 'FontFamily',
type: 'string',
component: 'dropdown',
label: 'FontFamily',
label: 'Font family',
options: [
{
value: 'QlikView Sans, -apple-system, sans-serif',
@@ -133,7 +174,7 @@ const tableFormat = {
},
DataFontSize: {
ref: 'lettersize',
translation: 'Font Size',
translation: 'Font size',
type: 'number',
component: 'buttongroup',
options: [
@@ -150,7 +191,7 @@ const tableFormat = {
},
textAlignment: {
ref: 'cellTextAlignment',
label: 'Cell Text alignment',
label: 'Cell text alignment',
component: 'buttongroup',
options: [
{
@@ -168,15 +209,33 @@ const tableFormat = {
],
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',
@@ -205,7 +264,7 @@ const tableFormat = {
ref: 'filteroncellclick',
type: 'boolean',
component: 'switch',
label: 'Filter data when cell clicked',
label: 'Allow selection in cells',
options: [
{
value: true,

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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'
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,19 +51,14 @@ 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: function (props, propName) {
if (props.isSnapshot || typeof props[propName] === 'function') {
@@ -65,6 +68,10 @@ ColumnHeader.propTypes = {
}
}).isRequired
}).isRequired,
entry: PropTypes.shape({
displayValue: PropTypes.string.isRequired,
elementNumber: PropTypes.number.isRequired
}).isRequired,
styling: PropTypes.shape({
headerOptions: PropTypes.shape({
fontSizeAdjustment: PropTypes.number.isRequired

View File

@@ -1,16 +1,17 @@
import React from 'react';
import PropTypes from 'prop-types';
import ExportButton from '../export-button.jsx';
import { HEADER_FONT_SIZE } from '../initialize-transformed';
import Tooltip from '../tooltip/index.jsx';
const ExportColumnHeader = ({ baseCSS, general, title, allowExcelExport, hasSecondDimension, styling }) => {
const Dim1Header = ({ component, baseCSS, title, hasSecondDimension, styling }) => {
const inEditState = component.inEditState();
const rowSpan = hasSecondDimension ? 2 : 1;
const isMediumFontSize = styling.headerOptions.fontSizeAdjustment === HEADER_FONT_SIZE.MEDIUM;
const style = {
...baseCSS,
cursor: 'default',
fontSize: `${16 + styling.headerOptions.fontSizeAdjustment}px`,
height: isMediumFontSize ? '100px' : '80px',
height: isMediumFontSize ? '90px' : '70px',
verticalAlign: 'middle',
width: '230px'
};
@@ -21,19 +22,20 @@ const ExportColumnHeader = ({ baseCSS, general, title, allowExcelExport, hasSeco
rowSpan={rowSpan}
style={style}
>
<ExportButton
excelExport={allowExcelExport}
general={general}
/>
{title}
<Tooltip
isTooltipActive={!inEditState}
styling={styling}
tooltipText={title}
>
{title}
</Tooltip>
</th>
);
};
ExportColumnHeader.propTypes = {
allowExcelExport: PropTypes.bool.isRequired,
Dim1Header.propTypes = {
baseCSS: PropTypes.shape({}).isRequired,
general: PropTypes.shape({}).isRequired,
component: PropTypes.shape({}).isRequired,
hasSecondDimension: PropTypes.bool.isRequired,
styling: PropTypes.shape({
headerOptions: PropTypes.shape({
@@ -43,4 +45,4 @@ ExportColumnHeader.propTypes = {
title: PropTypes.string.isRequired
};
export default ExportColumnHeader;
export default Dim1Header;

View File

@@ -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}
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,21 +126,12 @@ HeadersTable.propTypes = {
measurements: PropTypes.array
})
}).isRequired,
general: PropTypes.shape({}).isRequired,
qlik: PropTypes.shape({
backendApi: PropTypes.shape({
selectValues: function (props, propName) {
if (props.isSnapshot || typeof props[propName] === 'function') {
return null;
}
return new Error('Missing implementation of qlik.backendApi.selectValues.');
}
}).isRequired
}).isRequired,
component: PropTypes.shape({}).isRequired,
styling: PropTypes.shape({
headerOptions: PropTypes.shape({}),
options: PropTypes.shape({})
}).isRequired
}).isRequired,
isKpi: PropTypes.bool.isRequired
};
export default HeadersTable;

View File

@@ -3,62 +3,55 @@ 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 = styling.headerOptions.fontSizeAdjustment === HEADER_FONT_SIZE.MEDIUM;
const isMediumFontSize = fontSizeAdjustment === HEADER_FONT_SIZE.MEDIUM;
const cellStyle = {
...baseCSS,
verticalAlign: 'middle',
minWidth: cellWidth,
maxWidth: cellWidth
};
if (hasSecondDimension) {
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: isMediumFontSize ? '50px' : '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}
>
{title}
</Tooltip>
</span>
<Tooltip
tooltipText={title}
styling={styling}
>
{title}
</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: isMediumFontSize ? '100px' : '80px',
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>
);
};
@@ -69,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

View File

@@ -1,17 +1,19 @@
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 qlik from "qlik";
import React from "react";
import ReactDOM from "react-dom";
import Root from "./root.jsx";
import "./main.less";
if (!window._babelPolyfill) { // eslint-disable-line no-underscore-dangle
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 controller () {}
],
design: {
dimensions: {
max: 1,
@@ -20,16 +22,23 @@ export default {
},
data: {
dimensions: {
max: 3,
max (nMeasures) {
return nMeasures < 9 ? 2 : 1;
},
min: 1,
uses: 'dimensions'
},
measures: {
max: 8,
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,
@@ -37,11 +46,14 @@ export default {
qDimensions: [],
qInitialDataFetch: [
{
qHeight: 1,
qWidth: 10
qTop: 0,
qLeft: 0,
qWidth: 50,
qHeight: 50
}
],
qMeasures: []
qMeasures: [],
qSuppressZero: false
}
},
support: {
@@ -49,16 +61,76 @@ 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) {
const app = qlik.currApp(this);
const isPersonalResult = await app.global.isPersonalMode();
if (
!this.$scope.layout.allowexportxls ||
(isPersonalResult && isPersonalResult.qReturn)
) {
return menu;
}
if (typeof (this.backendApi.model.layout.qMeta.privileges[3]) !== 'undefined' && this.backendApi.model.layout.qMeta.privileges[3] === 'exportdata') {
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
};

View File

@@ -18,47 +18,16 @@ function getAlignment (option) {
function getFontSizeAdjustment (option) {
const fontSizeAdjustmentOptions = {
1: HEADER_FONT_SIZE.SMALL,
2: HEADER_FONT_SIZE.MEDIUM,
3: 2
2: HEADER_FONT_SIZE.MEDIUM
};
return fontSizeAdjustmentOptions[option] || 0;
}
function getCellSuffix (option) {
const cellSuffixOptions = {
1: '-s',
3: '-l'
};
return cellSuffixOptions[option] || '';
}
function getMeasurementFormat (measurement) {
if (measurement.qNumFormat.qType === 'U' || measurement.qNumFormat.qFmt === '##############') {
return '#.##0';
} else if (measurement.qNumFormat.qType === 'R') {
return measurement.qNumFormat.qFmt.replace(/(|)/gi, '');
}
return measurement.qNumFormat.qFmt;
}
function getMagnitudeLabelSuffix (magnitudeOption) {
const magnitudeLabelSuffixOptions = {
'k': ' (k)',
'm': ' (m)'
};
return magnitudeLabelSuffixOptions[magnitudeOption] || '';
}
function generateMeasurements (information) {
return information.map(measurement => {
const format = getMeasurementFormat(measurement);
const formatMagnitude = format.substr(format.length - 1).toLowerCase();
const transformedMeasurement = {
format,
magnitudeLabelSuffix: getMagnitudeLabelSuffix(formatMagnitude),
format: measurement.qNumFormat.qFmt || '#.##0',
name: measurement.qFallbackTitle
};
@@ -68,7 +37,7 @@ function generateMeasurements (information) {
function generateDimensionEntry (information, data) {
return {
displayValue: data.qText,
displayValue: data.qText || data.qNum,
elementNumber: data.qElemNumber,
name: information.qFallbackTitle,
value: data.qNum
@@ -79,11 +48,6 @@ function generateMatrixCell ({ cell, dimension1Information, dimension2Informatio
const matrixCell = {
displayValue: cell.qText,
format: measurementInformation.format,
magnitude: measurementInformation.magnitudeLabelSuffix.substring(
measurementInformation.magnitudeLabelSuffix.length - 2,
measurementInformation.magnitudeLabelSuffix.length - 1
),
magnitudeLabelSuffix: measurementInformation.magnitudeLabelSuffix,
name: measurementInformation.name,
parents: {
dimension1: {
@@ -91,7 +55,8 @@ function generateMatrixCell ({ cell, dimension1Information, dimension2Informatio
header: dimension1Information.qText
},
measurement: {
header: measurementInformation.name
header: measurementInformation.name,
index: measurementInformation.index
}
},
value: cell.qNum
@@ -99,103 +64,98 @@ function generateMatrixCell ({ cell, dimension1Information, dimension2Informatio
if (dimension2Information) {
matrixCell.parents.dimension2 = {
elementNumber: dimension2Information.qElemNumber
elementNumber: dimension2Information.qElemNumber,
header: dimension2Information.qText
};
}
return matrixCell;
}
let lastRow = 0;
function generateDataSet (component, dimensionsInformation, measurementsInformation, cubes) {
const dimension1 = [];
const dimension2 = [];
function generateDataSet (component, dimensionsInformation, measurementsInformation, dataCube) {
const measurements = generateMeasurements(measurementsInformation);
let dimension1 = [];
let dimension2 = [];
let matrix = [];
let previousDim1Entry;
const hasDesignDimension = cubes.design;
const hasSecondDimension = hasDesignDimension ? dimensionsInformation.length > 2 : dimensionsInformation.length > 1;
cubes.data.forEach(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
const hasSecondDimension = dimensionsInformation.length > 1;
// 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
};
}
function initializeTransformed ({ $element, component, cubes, layout }) {
function initializeTransformed ({ 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, cubes);
} = generateDataSet(component, dimensionsInformation, measurementsInformation, dataCube);
const customSchemaBasic = [];
const customSchemaFull = [];
let customHeadersCount = 0;
if (cubes.design) {
const allTextLines = cubes.design.map(entry => entry[0].qText);
const headers = allTextLines[0].split(';');
if (designList && designList.length > 0) {
const headers = designList[0].split(';');
customHeadersCount = headers.length;
for (let lineNumber = 0; lineNumber < allTextLines.length; lineNumber += 1) {
for (let lineNumber = 0; lineNumber < designList.length; lineNumber += 1) {
customSchemaFull[lineNumber] = new Array(headers.length);
const data = allTextLines[lineNumber].split(';');
const data = designList[lineNumber].split(';');
if (data.length === headers.length) {
for (let headerIndex = 0; headerIndex < headers.length; headerIndex += 1) {
@@ -206,6 +166,18 @@ function initializeTransformed ({ $element, component, cubes, layout }) {
}
}
let cellWidth;
if (layout.fitchartwidth) {
// The widths are calculated based on the current element width. Note: this could use % to set
// the widths as percentages of the available width. However, this often results in random
// columns getting 1px wider than the others because of rounding necessary to fill the width.
// This 1px causes missalignment between the data- and header tables.
cellWidth = '';
} else {
// If using the previous solution just set 60px
cellWidth = `${layout.columnwidthslider > 10 ? layout.columnwidthslider : 60}px`;
}
// top level properties could be reducers and then components connect to grab what they want,
// possibly with reselect for some presentational transforms (moving some of the presentational logic like formatting and such)
const transformedProperties = {
@@ -223,12 +195,12 @@ function initializeTransformed ({ $element, component, cubes, layout }) {
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)
@@ -239,7 +211,7 @@ function initializeTransformed ({ $element, component, cubes, layout }) {
count: customHeadersCount,
full: customSchemaFull
},
hasCustomFileStyle: Boolean(cubes.design),
hasCustomFileStyle: Boolean(designList),
headerOptions: {
alignment: getAlignment(layout.HeaderAlign),
colorSchema: layout.HeaderColorSchema.color,
@@ -254,62 +226,38 @@ function initializeTransformed ({ $element, component, cubes, layout }) {
fontSizeAdjustment: getFontSizeAdjustment(layout.lettersize),
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
],
metricsSpecificFields: layout.metricssemaphore.split(',').map(entry => Number(entry))
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.metricsstatus1,
medium: layout.metricsstatus2
},
statusColors: {
critical: {
backgroundColor: layout.colorstatus1.color,
color: layout.colorstatus1text.color
colors: {
poor: {
color: layout.conditionalcoloring.color_poor,
textColor: layout.conditionalcoloring.textcolor_poor
},
medium: {
backgroundColor: layout.colorstatus2.color,
color: layout.colorstatus2text.color
fair: {
color: layout.conditionalcoloring.color_fair,
textColor: layout.conditionalcoloring.textcolor_fair
},
normal: {
backgroundColor: layout.colorstatus3.color,
color: layout.colorstatus3text.color
good: {
color: layout.conditionalcoloring.color_good,
textColor: layout.conditionalcoloring.textcolor_good
}
}
},
symbolForNulls: layout.symbolfornulls,
usePadding: layout.indentbool,
useSeparatorColumns: dimensionCount === 1 ? false : layout.separatorcols
usePadding: layout.indentbool
}
};
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;
}

View File

@@ -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;

View File

@@ -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;
}
@@ -33,18 +30,17 @@
}
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;
border-collapse: collapse;
padding: 5px !important; // prevent overwriting from single object
white-space: nowrap;
overflow: hidden;
@@ -61,115 +57,19 @@
}
.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: inherit;
white-space: pre-line;
overflow: hidden;
display: block;
text-overflow: ellipsis;
}
.wrapclass45 {
width: 100%;
height: 45px;
white-space: pre-line;
overflow: hidden;
display: block;
}
.wrapclass70 {
width: 100%;
height: 70px;
white-space: pre-line;
overflow: hidden;
display: inline-block;
vertical-align: middle;
line-height: 20px;
}
.wrapclassEmpty {
width: 100%;
}
// *****************
// Medium column size
// *****************
.grid-cells {
position: relative;
._cell(70px);
}
.grid-cells2 {
._cell(70px);
}
.grid-cells-small {
._cell(52px);
}
.grid-cells2-small {
._cell(52px);
}
// *****************
// Small column size
// *****************
.grid-cells-s {
._cell(67px);
}
.grid-cells2-s {
._cell(67px);
}
.grid-cells-small-s {
._cell(52px);
}
.grid-cells2-small-s {
._cell(52px);
}
// *****************
// Large column size
// *****************
.grid-cells-l {
._cell(82px);
}
.grid-cells2-l {
._cell(82px);
}
.grid-cells-small-l {
._cell(66px);
}
.grid-cells2-small-l {
._cell(66px);
}
// END OF GRID CELLS
// First Column
.fdim-cells {
min-width: 230px !Important;
@@ -178,25 +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 {
height: 50px;
width: 350px;
@@ -210,33 +97,64 @@
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;
}
}
}
@@ -245,27 +163,50 @@
}
.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 {
// stylelint-disable-next-line property-no-unknown
scrollbar-width: none;

View File

@@ -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;
}

View File

@@ -1,28 +0,0 @@
import initializeStore from './store';
import React from 'react';
import ReactDOM from 'react-dom';
import Root from './root.jsx';
import { initializeCubes } from './dataset';
export default async function paint ($element, layout, component) {
const cubes = await initializeCubes({
component,
layout
});
const state = await initializeStore({
$element,
component,
cubes,
layout
});
const editmodeClass = component.inAnalysisState() ? '' : 'edit-mode';
const jsx = (
<Root
qlik={component}
state={state}
editmodeClass={editmodeClass}
/>
);
ReactDOM.render(jsx, $element[0]);
}

View File

@@ -1,60 +1,143 @@
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 {
constructor (props) {
super(props);
this.onDataTableRefSet = this.onDataTableRefSet.bind(this);
this.renderedTableWidth = 0;
}
componentDidUpdate () {
let tableWidth;
if (this.dataTableRef) {
tableWidth = this.dataTableRef.getBoundingClientRect().width;
if (this.renderedTableWidth !== tableWidth) {
this.forceUpdate();
}
}
}
onDataTableRefSet (element) {
this.dataTableRef = element;
this.forceUpdate();
}
render () {
const { editmodeClass, component, state } = this.props;
const { data, general, styling, error } = state;
// Determine cell- and column separator width
let cellWidth = '0px';
let columnSeparatorWidth = '';
if (this.dataTableRef) {
const tableWidth = this.dataTableRef.getBoundingClientRect().width;
this.renderedTableWidth = tableWidth;
if (general.cellWidth) {
cellWidth = general.cellWidth;
if (general.useColumnSeparator) {
columnSeparatorWidth = '8px';
}
} else {
const headerMarginRight = 8;
const borderWidth = 1;
const rowCellCount = data.matrix[0].length;
let separatorCount = 0;
let separatorWidth = 0;
if (general.useColumnSeparator) {
separatorCount = data.headers.dimension2.length - 1;
separatorWidth = Math.min(
Math.floor((tableWidth * 0.2) / separatorCount),
8
);
columnSeparatorWidth = `${separatorWidth}px`;
}
const separatorWidthSum =
(separatorWidth + borderWidth) * separatorCount;
cellWidth = `${Math.floor(
(tableWidth - separatorWidthSum - headerMarginRight - borderWidth) /
rowCellCount) - borderWidth}px`;
}
}
return (
<div className="root">
{error ? (
<div className={`error ${editmodeClass}`}>
{state.layout.errormessage}
</div>
) : (
<LinkedScrollWrapper>
<div className={`kpi-table ${editmodeClass}`}>
<HeadersTable
cellWidth={cellWidth}
columnSeparatorWidth={columnSeparatorWidth}
component={component}
data={data}
general={general}
isKpi
styling={styling}
/>
<LinkedScrollSection linkVertical>
<DataTable
cellWidth={cellWidth}
columnSeparatorWidth={columnSeparatorWidth}
component={component}
data={data}
general={general}
renderData={false}
styling={styling}
/>
</LinkedScrollSection>
</div>
<div
className={`data-table ${editmodeClass}`}
style={{ width: general.cellWidth ? 'auto' : '100%' }}
ref={this.onDataTableRefSet}
>
<LinkedScrollSection linkHorizontal>
<HeadersTable
cellWidth={cellWidth}
columnSeparatorWidth={columnSeparatorWidth}
component={component}
data={data}
general={general}
isKpi={false}
styling={styling}
/>
</LinkedScrollSection>
<LinkedScrollSection linkHorizontal linkVertical>
<DataTable
cellWidth={cellWidth}
columnSeparatorWidth={columnSeparatorWidth}
component={component}
data={data}
general={general}
styling={styling}
/>
</LinkedScrollSection>
</div>
</LinkedScrollWrapper>
)}
</div>
);
}
}
Root.propTypes = {
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;

View File

@@ -1,10 +1,11 @@
import initializeTransformed from './initialize-transformed';
async function initialize ({ $element, layout, component, cubes }) {
async function initialize ({ $element, layout, component, dataCube, designList }) {
const transformedProperties = await initializeTransformed({
$element,
component,
cubes,
dataCube,
designList,
layout
});

View File

@@ -25,23 +25,21 @@ class Tooltip extends React.PureComponent {
}
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>
@@ -56,6 +54,11 @@ Tooltip.propTypes = {
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired,
styling: PropTypes.shape({
options: PropTypes.shape({
fontFamily: PropTypes.string.isRequired
}).isRequired
}).isRequired,
tooltipText: PropTypes.string.isRequired
};

View File

@@ -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) => {

View File

@@ -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',

View File

@@ -4,11 +4,13 @@ const path = require('path');
const DIST = path.resolve("./dist");
const MODE = process.env.NODE_ENV || 'development';
const SOURCE_MAP = 'sourec-map';
const DEVTOOL = (process.env.NODE_ENV === '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: {