Compare commits

...

183 Commits

Author SHA1 Message Date
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
Albert Backenhof
141be3f962 Aligned build to Dashboard bundle extensions build
-Part of the work to streamline how the extensions
 are handled, irregardless of what bundle.

Issue: DEB-130, DEB-133
2019-03-27 09:53:09 +01:00
Albert Backenhof
433a725f33 Merge pull request #33 from qlik-oss/snapshotFix
New preview image and fixed snapshot
2019-03-20 10:28:52 +01:00
Albert Backenhof
769d5cfa3f New preview image and fixed snapshot
-SnapshotApi doesn't have a getProperties
 so fetching the source properties from
 qlik-app instead.

-The snapshot still cannot be rendered in the
 "printing-snapshot" test page because of
 limitations on that page.
2019-03-19 10:38:29 +01:00
Albert Backenhof
9d925b6205 Merge pull request #29 from qlik-oss/feature/QPE-427
[QPE-427] Replace csv styling with dimension styling
2019-03-18 06:57:13 +01: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
Kristoffer Lind
35d489c2e2 add updated demo app 2019-02-28 15:58:14 +01:00
Kristoffer Lind
9ef9981305 remove demo app 2019-02-28 15:56:44 +01:00
Kristoffer Lind
398192e057 fix indent and hide when design dimension is applied 2019-02-28 15:36:56 +01:00
Kristoffer Lind
4421217cb0 set default for metric semaphores to apply to nothing 2019-02-28 15:36:56 +01:00
Kristoffer Lind
c92be00ca7 update app with corrected colors for sheet 4 2019-02-28 15:36:56 +01:00
Kristoffer Lind
c66dfdc06c only show row colors when it does not have design dimension 2019-02-28 15:36:55 +01:00
Kristoffer Lind
a882b1d6aa update demo app so its no longer saved with faulty colors 2019-02-28 15:36:55 +01:00
Kristoffer Lind
554b029569 remove unused options include external csv file and path 2019-02-28 15:36:55 +01:00
Kristoffer Lind
4a15628325 remove csv files 2019-02-28 15:36:55 +01:00
Kristoffer Lind
fdb2aaaef4 use originHyperCubeDefinition as basis for dynamically created hyperCubes 2019-02-28 15:36:55 +01:00
Kristoffer Lind
a400e9c233 use createDimension for design cube aswell 2019-02-28 15:36:55 +01:00
Kristoffer Lind
091c564a75 apply styling from dimension 2019-02-28 15:36:54 +01:00
Kristoffer Lind
99eba8afcb split qHyperCube into two cubes, one for data and one for design 2019-02-28 15:36:54 +01:00
giovanni hanselius
cb78a2f2f9 Merge pull request #31 from qlik-oss/QPE-637-header-format-fixes
[QPE 637] Header format fixes
2019-02-28 12:48:09 +01:00
Balazs Gobel
f255efbf5d Handle medium header font size better and prevent cutting text off 2019-02-28 12:40:29 +01:00
giovanni hanselius
2f2d08fedd Merge pull request #30 from qlik-oss/QPE-636-table-format-fixes
[QPE 636] Table format fixes
2019-02-28 11:02:46 +01:00
Balazs Gobel
ac8b70bc84 Use correct fallback for all fonts 2019-02-28 10:49:24 +01:00
giovanni hanselius
0f2a4f9805 Merge pull request #27 from qlik-oss/QPE-631-IE-errors
[QPE 631] Prevent console errors in IE on cell hover
2019-02-28 09:33:55 +01:00
Balazs Gobel
85228412cc show correct color in the color picker 2019-02-27 21:43:09 +01:00
Balazs Gobel
03dfc0ce93 prevent infinite loop in angular color picker
- dualOutput must be the last line
2019-02-27 21:39:59 +01:00
Balazs Gobel
48427df559 enable changing font size in headers 2019-02-27 21:39:31 +01:00
Balazs Gobel
7fda7aa2d9 better defaults when missing font family 2019-02-27 20:54:50 +01:00
Balazs Gobel
4ba12b8b2d show user-selected font-family for the cells 2019-02-27 20:51:18 +01:00
Balazs Gobel
07af7b509e Make table format work again
- working color picker
 - working font size
 - working cell text alignment
 - this is all by preventing the infinite loop in angular
2019-02-27 20:45:06 +01:00
Balazs Gobel
b7ff83e1da prevent console errors in IE on cell hover 2019-02-27 20:37:02 +01:00
giovanni hanselius
621359d6f9 Merge pull request #28 from qlik-oss/QPE-634-column-width-fix
[QPE 634] Fix column width
2019-02-27 15:06:04 +01:00
Balazs Gobel
a71f80f8fa minor cleanup 2019-02-27 14:26:27 +01:00
Balazs Gobel
a6cbfcda70 Fix column width issue
- Look at the value format to determine if column is percentage based
2019-02-27 14:25:50 +01:00
giovanni hanselius
de2e9c16ac Merge pull request #25 from qlik-oss/QPE-549-tooltip-design
[QPE 549] Minor adjustments for tooltip
2019-02-27 12:30:42 +01:00
giovanni hanselius
ad0c0dacba Merge pull request #24 from qlik-oss/QPE-622-visible-scrollbar
[QPE 622] show scrollbar for easier scrolling
2019-02-27 11:14:01 +01:00
giovanni hanselius
97564cf8b1 Merge pull request #26 from qlik-oss/QPE-600-same-padding-in-single-object-mode
[QPE 600] Use same layout in single onject mode as in normal mode
2019-02-27 11:03:33 +01:00
Balazs Gobel
7fa44c06b0 Use same layout in single onject mode as in normal mode
- same padding for single object mode
2019-02-26 16:02:40 +01:00
Balazs Gobel
dae192b7af Minor adjustments for tooltip
- Added tooltip for row header
- Better vertical alignment
- Move static styling to css
2019-02-26 15:44:17 +01:00
Balazs Gobel
5abfd5b7e5 show scrollbar for easier scrolling 2019-02-26 15:10:00 +01:00
giovanni hanselius
65f5d70654 Merge pull request #23 from qlik-oss/feature/QPE-615
[QPE-615] fix metric semaphore colors
2019-02-26 14:34:17 +01:00
Kristoffer Lind
da7ba5c3a8 fix default indexes for metric colors 2019-02-26 14:23:42 +01:00
Kristoffer Lind
6e5df323d8 update sample state 2019-02-26 11:37:31 +01:00
Kristoffer Lind
aad92678db fix metric semaphore colors 2019-02-26 11:31:24 +01:00
giovanni hanselius
15226d8598 Merge pull request #17 from qlik-oss/QPE-426
[QPE-426] color pickers
2019-02-25 15:25:29 +01:00
Balazs Gobel
a5fc586859 Merge branch 'master' into QPE-426 2019-02-25 15:14:13 +01:00
giovanni hanselius
980c0387bf Merge pull request #14 from qlik-oss/feature/QPE-549
[QPE-549] tooltip
2019-02-25 14:45:13 +01:00
Balazs Gobel
710d4a8842 Fix tests 2019-02-25 13:41:11 +01:00
Balazs Gobel
ebb5a7cf16 Additional merge conflict fixes 2019-02-25 12:59:02 +01:00
Balazs Gobel
7107f485be resolve additional merge conflict: removed obsolete code 2019-02-25 12:55:48 +01:00
Balazs Gobel
633fd39b80 Merge branch 'master' into feature/QPE-549
# Conflicts:
#	src/main.less
#	src/paint.jsx
2019-02-25 12:50:47 +01:00
Christopher Lebond
9eeaecb994 Merge pull request #22 from qlik-oss/fix-merge-issues
remove some duplicated css from merge conflicts
2019-02-22 16:11:48 +01:00
Kristoffer Lind
7305175049 fix semaphore alignment 2019-02-22 16:08:51 +01:00
Kristoffer Lind
f99281ff47 hide scrollbars in firefox 2019-02-22 16:04:24 +01:00
Kristoffer Lind
557cd1aeb6 remove some duplicated css 2019-02-22 16:04:24 +01:00
Christopher Lebond
ca5f442fe0 Merge pull request #8 from qlik-oss/feature/QPE-585
[QPE 585] Minor design fixes
2019-02-22 15:37:56 +01:00
ahmed-Bazzara
585243bb73 merge confloicts solved 2019-02-22 14:49:36 +01:00
Kristoffer Lind
a25b2c40c0 removed a few more pieces of the colors library 2019-02-22 14:30:18 +01:00
ahmed-Bazzara
46d6520273 update app added
colors set to have a fallback value
2019-02-22 14:29:23 +01:00
ahmed-Bazzara
19286f6c56 color libraries deleted and Qlik's color-pickers introduced 2019-02-22 14:29:23 +01:00
Ahmed Bazzara
8b760646ab Merge pull request #12 from qlik-oss/feature/QPE-564
[QPE-564] test setup for components and some initial tests
2019-02-22 14:21:04 +01:00
ahmed-Bazzara
b5f25e5e18 babel/plugin-proposal-class-properties added to karma.conf 2019-02-22 14:17:08 +01:00
Ahmed Bazzara
b335b4883e Merge pull request #9 from qlik-oss/feature/QPE-586-stylelint
[QPE 586] Added stricter stylelint
2019-02-22 13:51:53 +01:00
ahmed-Bazzara
94e4203a0b merge conflicts solved 2019-02-22 13:51:09 +01:00
ahmed-Bazzara
221e2d365c merge conflicts solved 2019-02-22 13:45:19 +01:00
Christopher Lebond
6797f7d561 Merge pull request #13 from qlik-oss/feature/QPE-569-change-eslint-rules
[QPE-569] Fix most of the eslint warnings
2019-02-22 13:21:45 +01:00
Christopher Lebond
f843779b64 Merge pull request #21 from qlik-oss/feature/QPE-474
[QPE-474] replace jquery scroll linking
2019-02-22 13:13:57 +01:00
ahmed-Bazzara
951d534343 merge conflicts solved 2019-02-22 12:59:57 +01:00
ahmed-Bazzara
c5acb43a7a merge conflicts solved 2019-02-22 12:59:38 +01:00
ahmed-Bazzara
979c036b49 settings file reverted 2019-02-22 12:44:44 +01:00
ahmed-Bazzara
63c877face merge conflicts solved 2019-02-22 12:40:30 +01:00
ahmed-Bazzara
9809587c68 merge conflicts solved 2019-02-22 12:12:00 +01:00
ahmed-Bazzara
808f4df3e3 merge conflicts solved 2019-02-22 12:01:41 +01:00
ahmed-Bazzara
bbadc711dc merge conflicts resolved 2019-02-22 11:56:38 +01:00
Christopher Lebond
47b4d1aa5b Merge pull request #15 from qlik-oss/feature/QPE-484
[QPE-484] Edit mode interactions
2019-02-22 11:28:35 +01:00
ahmed-Bazzara
18e2b2024e fixing alignment between cells and row-headers 2019-02-22 11:26:17 +01:00
Christopher Lebond
614d768eea Merge pull request #10 from qlik-oss/QPE-477
[QPE-477] Definition object is Qlik standard
2019-02-22 10:41:36 +01:00
Kristoffer Lind
807a609a90 fix rebase issue 2019-02-22 10:24:41 +01:00
Kristoffer Lind
82257be3a8 fix review comments 2019-02-21 18:49:07 +01:00
Kristoffer Lind
734fe33537 update sample state 2019-02-21 18:48:42 +01:00
Kristoffer Lind
347e6b7408 test setup for components and some initial tests 2019-02-21 18:38:30 +01:00
ahmed-Bazzara
bdf231f88d code enhancements 2019-02-21 18:17:45 +01:00
ahmed-Bazzara
88ad73bd41 tooltip position now follow the mouse 2019-02-21 18:17:45 +01:00
ahmed-Bazzara
530f0919f1 tooltip positioning tweaked 2019-02-21 18:17:45 +01:00
ahmed-Bazzara
79b89a3b25 all logic for tooltip has been moved to it's component
data-cell & column-header components reseted to pure ones
2019-02-21 18:17:44 +01:00
ahmed-Bazzara
4520d6a48a handling tooltip logic within it 2019-02-21 18:17:44 +01:00
ahmed-Bazzara
98678d4a13 jQuery commented code deleted 2019-02-21 18:17:44 +01:00
ahmed-Bazzara
da57204c41 console log removed 2019-02-21 18:17:44 +01:00
ahmed-Bazzara
906a11c6b4 tooltip component cleaned 2019-02-21 18:17:44 +01:00
ahmed-Bazzara
c66ad78e48 tooltip added to column header
comopnent changed to normal component to hold state in it
2019-02-21 18:17:44 +01:00
ahmed-Bazzara
6994bf51a3 showTooltip boolean changed name
skipping values that are header rows
2019-02-21 18:17:44 +01:00
ahmed-Bazzara
521d508604 commented jqeury from paint.js for tooltip 2019-02-21 18:17:43 +01:00
ahmed-Bazzara
3946f6c894 mind width set to tooltip
text inside it is center aligned
2019-02-21 18:16:38 +01:00
ahmed-Bazzara
aeccbf5d17 tooltip disabled in edit state 2019-02-21 18:16:38 +01:00
ahmed-Bazzara
95e330a50d tooltip added 2019-02-21 18:16:38 +01:00
Kristoffer Lind
555000be54 fix rebase issues 2019-02-21 17:49:05 +01:00
Kristoffer Lind
c367f24dd9 replace jquery scroll linking with components (fixing some bugs and getting rid of jquery) 2019-02-21 17:38:51 +01:00
giovanni hanselius
0b3b7b3f57 Merge pull request #19 from qlik-oss/QPE-554
[QPE-554] text alignemt
2019-02-21 17:37:00 +01:00
giovanni hanselius
44b33b4c92 Merge pull request #16 from qlik-oss/feature/QPE-550
[QPE-550] fix excel export
2019-02-21 16:48:34 +01:00
giovanni hanselius
61b339b146 Merge pull request #18 from qlik-oss/QPE-479
[QPE-479] Qlik defaults
2019-02-21 15:42:31 +01:00
ahmed-Bazzara
27b84c5623 code enhancements 2019-02-21 12:29:16 +01:00
ahmed-Bazzara
24edf1c6f4 values of text alignement property set to have lowercase 2019-02-21 12:02:42 +01:00
giovanni hanselius
fc363d7739 Merge pull request #20 from qlik-oss/export
exporting image and PDF enabled
2019-02-20 16:01:27 +01:00
ahmed-Bazzara
35d4dde118 exporting image and PDF enabled 2019-02-20 15:50:54 +01:00
ahmed-Bazzara
e70e76a401 small font size is set to be default
and its value matched to Qlik defalut font size
2019-02-20 15:44:36 +01:00
ahmed-Bazzara
a804c31658 tooltip position now follow the mouse 2019-02-19 16:58:39 +01:00
ahmed-Bazzara
9efe580d18 tooltip positioning tweaked 2019-02-19 15:41:30 +01:00
ahmed-Bazzara
bcb9d30237 sending the alignment value straight from props instead of numbers 2019-02-19 14:55:50 +01:00
Kristoffer Lind
fa60dd5248 remove unused component 2019-02-19 10:25:14 +01:00
Balazs Gobel
fd653de0e1 Fix most of the eslint warnings 2019-02-19 10:23:24 +01:00
ahmed-Bazzara
ec140efc56 Text alignment property added 2019-02-18 16:42:39 +01:00
ahmed-Bazzara
34477d7ef1 qlik font added to the fonts dropdown
and was made a default value
2019-02-18 16:12:40 +01:00
Tobias Åström
b65d1c51fc Update qlik-smart-pivot.qext 2019-02-16 10:49:46 +01:00
Tobias Åström
9111ec762b Update config.yml 2019-02-16 10:48:49 +01:00
ahmed-Bazzara
48970cb4f4 all logic for tooltip has been moved to it's component
data-cell & column-header components reseted to pure ones
2019-02-15 11:33:43 +01:00
ahmed-Bazzara
fe4b5a72ec handling tooltip logic within it 2019-02-15 09:56:32 +01:00
ahmed-Bazzara
8b41022ddc jQuery commented code deleted 2019-02-15 09:32:55 +01:00
ahmed-Bazzara
9c66c09899 console log removed 2019-02-15 09:26:16 +01:00
ahmed-Bazzara
09d9055643 tooltip component cleaned 2019-02-15 08:56:26 +01:00
ahmed-Bazzara
b1204e0929 tooltip added to column header
comopnent changed to normal component to hold state in it
2019-02-15 08:56:02 +01:00
ahmed-Bazzara
d130ca266d showTooltip boolean changed name
skipping values that are header rows
2019-02-15 08:55:13 +01:00
ahmed-Bazzara
f730dc2827 commented jqeury from paint.js for tooltip 2019-02-15 08:54:11 +01:00
ahmed-Bazzara
b5f74395f7 mind width set to tooltip
text inside it is center aligned
2019-02-15 08:53:35 +01:00
Kristoffer Lind
b86806d4cd cleanup tooltips (resulted in whatever header was last hovered to be appended to each column header in xls) 2019-02-14 15:14:23 +01:00
ahmed-Bazzara
dfac9ad5e9 tooltip disabled in edit state 2019-02-14 14:04:15 +01:00
ahmed-Bazzara
db67b864ee edit mode interaction prevented 2019-02-14 13:54:17 +01:00
ahmed-Bazzara
377d642fe2 tooltip added 2019-02-14 13:02:59 +01:00
Kristoffer Lind
c3651a37da Merge branch 'master' into feature/QPE-550 2019-02-14 12:36:55 +01:00
Kristoffer Lind
8b843e028a fix excel export 2019-02-14 11:08:34 +01:00
John Lunde
2bdd98aaca Merge pull request #7 from qlik-oss/feature/QPE-563
[QPE-563] Let react handle rendering
2019-02-14 10:38:32 +01:00
Kristoffer Lind
d723451656 revert accidental design change 2019-02-13 13:52:20 +01:00
ahmed-Bazzara
c47b401a1d typo in definiton object 2019-02-12 12:05:01 +01:00
ahmed-Bazzara
3c330465dd Definition object is Qlik standard 2019-02-12 12:03:05 +01:00
Kristoffer Lind
f2f201c6e2 let react handle rendering 2019-02-12 11:26:31 +01:00
Balazs Gobel
8984affe87 minor cleanup 2019-02-12 11:25:22 +01:00
Balazs Gobel
ce1e196b78 Follow the same principle as the old application
- Restyle table cells only when metric semaphore is enabled
2019-02-12 11:25:22 +01:00
Balazs Gobel
e3b7a7640e Fixed misalignment when it's only one dimension 2019-02-12 11:25:22 +01:00
Balazs Gobel
68dccf8575 Fix pixel misalignment between table header and body 2019-02-12 11:25:22 +01:00
Balazs Gobel
8e100f286b Fix alternating colours 2019-02-12 11:25:22 +01:00
Balazs Gobel
f7ceb5c2bf Added stricter stylint rules 2019-02-12 11:24:13 +01:00
John Lunde
8e1394e898 Merge pull request #6 from qlik-oss/feature/QPE-548
[QPE-548] refactor focusing on data structure
2019-02-11 15:23:02 +01:00
Kristoffer Lind
c69cfec533 Merge branch 'master' into feature/QPE-548 2019-02-07 13:49:21 +01:00
Kristoffer Lind
c4a717bd77 fix single dimension custom style headers 2019-02-07 11:12:49 +01:00
Kristoffer Lind
31e9be5220 Merge pull request #5 from qlik-oss/feature/QPE-575-multi-load-babel
[QPE 575] Load babel only once for a page
2019-02-07 10:43:22 +01:00
Kristoffer Lind
b979a579f9 refactor row-wrapper according to new data structure 2019-02-07 10:30:41 +01:00
Kristoffer Lind
6233b9dbae refactor header-wrapper according to new data structure 2019-02-07 10:20:19 +01:00
Kristoffer Lind
9b682e62f8 create new data structure 2019-02-07 10:19:42 +01:00
Balazs Gobel
462b4a13a2 Load babel only once for a page
- Fixed when having multiple visualisations on one page
- Babel will load only once
2019-02-01 12:22:25 +01:00
64 changed files with 3134 additions and 5179 deletions

View File

@@ -19,8 +19,13 @@ jobs:
name: Install dependencies
command: npm install
- run:
name: Run tests
command: npm run test-once
name: BlackDuck scan
command: curl -s https://blackducksoftware.github.io/hub-detect/hub-detect.sh | bash -s -- \
--blackduck.url="https://qliktech.blackducksoftware.com" \
--blackduck.trust.cert=true \
--blackduck.username="svc-blackduck" \
--blackduck.password=${svc_blackduck} \
--detect.project.name="viz-bundle-qlik-smart-pivot"
bump-version:
<<: *defaults
@@ -48,16 +53,18 @@ jobs:
command: |
export VERSION=$(scripts/get-bumped-version.sh)
echo "Version: ${VERSION}"
npm run build
npm run build:zip
sudo chmod +x scripts/verify-files.sh
scripts/verify-files.sh
environment:
NODE_ENV: production
- persist_to_workspace:
root: ~/qlik-smart-pivot
paths:
- build
- dist
- store_artifacts:
path: build
destination: build
path: dist
destination: dist
deploy:
<<: *defaults
steps:

View File

@@ -15,6 +15,7 @@ module.exports = {
},
globals: {
angular: false,
beforeEach: false,
define: false,
describe: false,
document: false,
@@ -41,12 +42,12 @@ module.exports = {
"no-cond-assign": ["warn"],
"no-fallthrough": ["warn"],
"no-undef": ["error"],
"no-unused-vars": ["warn"],
"no-use-before-define": ["warn", { "functions": false, "classes": false, "variables": false }],
"no-unused-vars": ["error"],
"no-use-before-define": ["error", { "functions": false, "classes": false, "variables": false }],
"no-useless-escape": ["warn"],
"no-useless-return": ["warn"],
"no-underscore-dangle": ["warn", { "allow": ["_id"] }],
"no-redeclare": ["warn"],
"no-redeclare": ["error"],
"no-restricted-syntax": ["warn"],
"operator-linebreak": ["warn", "before"],
"prefer-promise-reject-errors": ["warn"],
@@ -63,11 +64,11 @@ module.exports = {
"complexity": ["warn"],
"camelcase": ["warn"],
"max-statements": ["off"], // marks the entire functions, a bit too noisy
"sort-vars": ["warn"],
"sort-vars": ["off"], // not much value for the work
"init-declarations": ["off"],
"capitalized-comments": ["off"],
"one-var": ["off"],
"no-var": ["warn"],
"no-var": ["error"],
"no-plusplus": ["warn"],
"vars-on-top": ["off"],
"no-magic-numbers": ["off"], // useful, but also complains for reasonable checks with actual numbers
@@ -80,7 +81,7 @@ module.exports = {
"quote-props": ["off"],
"prefer-template": ["warn"],
"no-lonely-if": ["warn"],
"sort-keys": ["warn"],
"sort-keys": ["off"], // not much value for the work
"no-implicit-coercion": ["warn"],
"no-inline-comments": ["off"],
"spaced-comment": ["warn"],
@@ -107,7 +108,7 @@ module.exports = {
"strict": ["warn"],
"no-ternary": ["off"],
"multiline-ternary": ["off"],
"no-param-reassign": ["warn"],
"no-param-reassign": ["error"],
"prefer-destructuring": ["warn"],
"arrow-parens": ["off"],
"no-array-constructor": ["warn"],
@@ -116,7 +117,7 @@ module.exports = {
"max-params": ["warn"],
"brace-style": ["warn", "1tbs", { "allowSingleLine": true }],
"prefer-const": ["warn"],
"class-methods-use-this":["warn"],
// plugin:react
"react/jsx-indent": ["warn", 2],
"react/jsx-indent-props": ["warn", 2],
@@ -129,7 +130,8 @@ module.exports = {
"react/jsx-no-literals": ["off"],
"react/jsx-max-depth": ["off"], // rule throws exception in single-dimension-measure
"react/jsx-filename-extension": ["warn"],
"react/prefer-stateless-function": ["warn"]
"react/prefer-stateless-function": ["warn"],
"react/no-set-state": ["warn"]
},
extends: [
"eslint:all",

2
.gitignore vendored
View File

@@ -19,7 +19,7 @@ $RECYCLE.BIN/
# Temporary build files
node_modules/
build/
dist/
BUMPED_VERSION
# =========================

View File

@@ -16,11 +16,6 @@ It's specifically focused on financial reports, trying to solve some common need
You'll find a manual [Qlik Sense P&LSmart Pivot Extension Manual.pdf](resources/Qlik Sense P&LSmart Pivot Extension Manual.pdf) and one app example [P&LSmartPivot_demo.qvf](resources/P&LSmartPivot_demo.qvf).
# If the import does not work at first time
- remove [Accounts.csv](resources/Accounts.csv), [Accounts2.csv](resources/Accounts2.csv) and [Excel.png](resources/Excel.png), zip it again and import.
- Then reintroduce [Accounts.csv](resources/Accounts.csv), [Accounts2.csv](resources/Accounts2.csv) and [Excel.png](resources/Excel.png), zip it again and import.
# Installation
1. Download the extension zip, `qlik-smart-pivot_<version>.zip`, from the latest release(https://github.com/qlik-oss/PLSmartPivot/releases/latest)

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 B

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@@ -1,16 +0,0 @@
{
"name": "Smart pivot",
"description": "Formatted table for P&L reports.",
"type": "visualization",
"version": "X.Y.Z",
"icon": "table",
"preview": "",
"author": "Ivan Felipe Asensio <ivan.felipe@qlik.com>",
"homepage": "",
"keywords": "qlik-sense, visualization",
"license": "MIT",
"repository": "https://github.com/qlik-oss/PLSmartPivot",
"dependencies": {
"qlik-sense": ">=5.5.x"
}
}

View File

@@ -1,3 +0,0 @@
qlik-smart-pivot.js;
qlik-smart-pivot.css;
qlik-smart-pivot.qext

View File

@@ -1,23 +1,61 @@
var gulp = require('gulp');
var gutil = require('gulp-util');
var zip = require('gulp-zip');
var del = require('del');
var path = require('path');
var settings = require('./settings');
var webpackConfig = require('./webpack.config');
var webpack = require('webpack');
var WebpackDevServer = require('webpack-dev-server');
var jeditor = require("gulp-json-editor");
var pkg = require('./package.json');
var srcFiles = path.resolve('./src/**/*.*');
var DIST = './dist';
var VERSION = process.env.VERSION || 'local-dev';
gulp.task('remove-build-folder', function(){
return del([settings.buildDestination], { force: true });
gulp.task('qext', function () {
var qext = {
name: 'P&L pivot',
type: 'visualization',
description: pkg.description + '\nVersion: ' + VERSION,
version: VERSION,
icon: 'pivot-table',
preview: 'qlik-smart-pivot.png',
keywords: 'qlik-sense, visualization',
author: pkg.author,
homepage: pkg.homepage,
license: pkg.license,
repository: pkg.repository,
dependencies: {
'qlik-sense': '>=5.5.x'
}
};
if (pkg.contributors) {
qext.contributors = pkg.contributors;
}
var src = require('stream').Readable({
objectMode: true
});
src._read = function () {
this.push(new gutil.File({
cwd: '',
base: '',
path: pkg.name + '.qext',
contents: Buffer.from(JSON.stringify(qext, null, 4))
}));
this.push(null);
};
return src.pipe(gulp.dest(DIST));
});
gulp.task('clean', function(){
return del([DIST], { force: true });
});
gulp.task('zip-build', function(){
return gulp.src(settings.buildDestination + '/**/*')
.pipe(zip(`${settings.name}_${settings.version}.zip`))
.pipe(gulp.dest(settings.buildDestination));
return gulp.src(DIST + '/**/*')
.pipe(zip(`${pkg.name}_${VERSION}.zip`))
.pipe(gulp.dest(DIST));
});
gulp.task('add-assets', function(){
return gulp.src('./assets/**/*').pipe(gulp.dest(DIST));
});
gulp.task('webpack-build', done => {
@@ -36,40 +74,14 @@ gulp.task('webpack-build', done => {
});
});
gulp.task('update-qext-version', function () {
return gulp.src(`${settings.buildDestination}/${settings.name}.qext`)
.pipe(jeditor({
'version': settings.version
}))
.pipe(gulp.dest(settings.buildDestination));
});
gulp.task('build',
gulp.series('remove-build-folder', 'webpack-build', 'update-qext-version', 'zip-build')
gulp.series('clean', 'webpack-build', 'qext', 'add-assets')
);
gulp.task('zip',
gulp.series('build', 'zip-build')
);
gulp.task('default',
gulp.series('build')
);
gulp.task('watch', () => new Promise((resolve, reject) => {
webpackConfig.entry.unshift('webpack-dev-server/client?http://localhost:' + settings.port);
const compiler = webpack(webpackConfig);
const originalOutputFileSystem = compiler.outputFileSystem;
const devServer = new WebpackDevServer(compiler, {
headers: {
"Access-Control-Allow-Origin": "*"
},
}).listen(settings.port, 'localhost', error => {
compiler.outputFileSystem = originalOutputFileSystem;
if (error) {
console.error(error); // eslint-disable-line no-console
return reject(error);
}
// eslint-disable-next-line no-console
console.log('Listening at localhost:' + settings.port);
resolve(null, devServer);
});
}));

View File

@@ -1,43 +0,0 @@
const settings = require('./settings');
module.exports = (config) => {
config.set({
browsers: ['SlimChromeHeadless'],
customLaunchers: {
SlimChromeHeadless: {
base: 'ChromeHeadless',
flags: ['--headless', '--disable-gpu', '--disable-translate', '--disable-extensions']
}
},
files: [
{ pattern: 'src/*.spec.js', watched: false }
],
frameworks: ['jasmine'],
preprocessors: {
'src/*.spec.js': ['webpack', 'sourcemap']
},
webpack: {
devtool: 'source-map',
mode: settings.mode,
externals: {
jquery: {
amd: 'jquery',
commonjs: 'jquery',
commonjs2: 'jquery',
root: '_'
},
},
module: {
rules: [
{
test: /\.js$/,
exclude: [/node_modules/],
loaders: ['babel-loader']
},
{ test: /\.less$/, loader: 'ignore-loader' },
{ test: /\.json$/, loader: 'ignore-loader' }
]
}
}
});
};

2979
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,51 +1,44 @@
{
"name": "qlik-smart-pivot",
"version": "0.0.1",
"description": "Formatted table for P&L reports.",
"keywords": "smart pivot qliksense extension",
"description": "Profit & Loss reporting with color and font customizations.",
"homepage": "",
"repository": "https://github.com/iviasensio/PLSmartPivot",
"author": "Ivan Felipe Asensio <ivan.felipe@qlik.com>",
"license": "MIT",
"scripts": {
"build": "gulp build",
"build:zip": "gulp zip",
"eslint": "eslint src",
"eslint:fix": "eslint --fix src",
"test": "karma start karma.conf.js",
"test-once": "karma start karma.conf.js --single-run",
"watch": "gulp watch"
"stylelint": "stylelint src/main.less"
},
"devDependencies": {
"@babel/core": "7.1.2",
"@babel/plugin-proposal-class-properties": "7.3.3",
"@babel/plugin-transform-async-to-generator": "7.1.0",
"@babel/polyfill": "7.0.0",
"@babel/preset-env": "7.1.0",
"@babel/preset-react": "7.0.0",
"babel-eslint": "10.0.1",
"babel-loader": "8.0.4",
"copy-webpack-plugin": "4.5.3",
"css-loader": "1.0.0",
"del": "2.0.2",
"enzyme": "3.8.0",
"enzyme-adapter-react-16": "1.9.1",
"eslint": "5.7.0",
"eslint-loader": "2.1.1",
"eslint-plugin-react": "7.11.1",
"gulp": "4.0.0",
"gulp-json-editor": "2.4.3",
"gulp-util": "^3.0.7",
"gulp-zip": "3.0.2",
"jasmine-core": "3.2.1",
"karma": "3.0.0",
"karma-chrome-launcher": "2.2.0",
"karma-jasmine": "1.1.2",
"karma-sourcemap-loader": "0.3.7",
"karma-webpack": "3.0.5",
"less": "3.8.1",
"less-loader": "4.1.0",
"lodash.merge": "4.6.1",
"style-loader": "0.23.1",
"stylelint": "8.4.0",
"stylelint-webpack-plugin": "0.10.5",
"text-loader": "0.0.1",
"webpack": "4.20.2",
"webpack-cli": "3.1.2",
"webpack-dev-server": "3.1.10"
"webpack": "4.20.2"
},
"dependencies": {
"prop-types": "15.6.2",

View File

@@ -1,44 +0,0 @@
Accounts;Bold;Background;FontStyle;LetterColor;Align;Size;Comment
Revenues;;rgb(183, 219, 255);<italic>;;<center>;;<comment>
Gross sales revenues;;;;;;;
Less return & allowances;;;;;;;
Net sales revenues;<bold>;<soft>;;;;;
Cost of goods sold;<bold>;;;;;;
Direct materials;;;;;;;
Direct labor;;;;;;;
Manufacturing overhead;;#b7dbff;<italic>;;<center>;;<comment>
Indirect labor;;;;;;;
Depreciation, manufacturing equip;;;;;;;
Other mfr overhead;;;;;;;
Net mfr overhead;<bold>;<violete>;;;;;
Net costs of goods sold;<bold>;<violete>;;;;;
Gross profit;<bold>;<dark>;;;<center>;<large>;
Operating expenses;<bold>;;<italic>;;;;
Selling expenses;;;;;;;
Sales salaries;;;;;;;
Warranty expenses;;;;;;;
Depreciation, store equipment;;;;;;;
Other selling expenses3;;;;;;;
Total selling expenses;<bold>;<violete>;;;;;
General & administrative expenses;;#b7dbff;<italic>;;<center>;;<comment>
Administration salaries;;;;;;;
Rent expenses;;;;;;;
Depreciation, computers;;;;;;;
Other general & admin expenses;;;;;;;
total general & admin expenses;<bold>;<soft>;;;;;
total operating expenses;<bold>;<violete>;;;;;
Operating income before taxes;<bold>;<dark>;;<white>;;<large>;
Financial revenue & expenses;;#b7dbff;<italic>;;<center>;;<comment>
Revenue from investments;;;;;;;
Less interest expenses;;;;;;;
Net financial gain (expense);;;;;;;
Income before tax & extraordinary items;<bold>;<dark>;;<white>;;<large>;
Less income tax on operations;;;;;;;
Income before extraordinary items;;;;;;;
Extraordinary items;;#b7dbff;<italic>;;<center>;;<comment>
Sale of land;;;;;;;
Less initial cost;;;;;;;
Net gain on sale of land;<bold>;<soft>;;;;;
Less income tax on gain;;;;;;;
Extraordinary items after tax;<bold>;<soft>;;;;;
Net Income (Profit);<bold>;<dark>;;<white>;<center>;<large>;
1 Accounts Bold Background FontStyle LetterColor Align Size Comment
2 Revenues rgb(183, 219, 255) <italic> <center> <comment>
3 Gross sales revenues
4 Less return & allowances
5 Net sales revenues <bold> <soft>
6 Cost of goods sold <bold>
7 Direct materials
8 Direct labor
9 Manufacturing overhead #b7dbff <italic> <center> <comment>
10 Indirect labor
11 Depreciation, manufacturing equip
12 Other mfr overhead
13 Net mfr overhead <bold> <violete>
14 Net costs of goods sold <bold> <violete>
15 Gross profit <bold> <dark> <center> <large>
16 Operating expenses <bold> <italic>
17 Selling expenses
18 Sales salaries
19 Warranty expenses
20 Depreciation, store equipment
21 Other selling expenses3
22 Total selling expenses <bold> <violete>
23 General & administrative expenses #b7dbff <italic> <center> <comment>
24 Administration salaries
25 Rent expenses
26 Depreciation, computers
27 Other general & admin expenses
28 total general & admin expenses <bold> <soft>
29 total operating expenses <bold> <violete>
30 Operating income before taxes <bold> <dark> <white> <large>
31 Financial revenue & expenses #b7dbff <italic> <center> <comment>
32 Revenue from investments
33 Less interest expenses
34 Net financial gain (expense)
35 Income before tax & extraordinary items <bold> <dark> <white> <large>
36 Less income tax on operations
37 Income before extraordinary items
38 Extraordinary items #b7dbff <italic> <center> <comment>
39 Sale of land
40 Less initial cost
41 Net gain on sale of land <bold> <soft>
42 Less income tax on gain
43 Extraordinary items after tax <bold> <soft>
44 Net Income (Profit) <bold> <dark> <white> <center> <large>

View File

@@ -1,21 +0,0 @@
Accounts;Bold;Background;FontStyle;LetterColor;Align;Size;Comment
Revenues;<bold>;;<italic>;;<center>;;<comment>
Net sales revenues;<bold>;rgb(128, 191, 255);;;;;
Cost of goods sold;<bold>;rgb(128, 191, 255);;;;;
Manufacturing overhead;<bold>;;<italic>;;<center>;;<comment>
Net mfr overhead;<bold>;rgb(128, 191, 255);;;;;
Net costs of goods sold;<bold>;rgb(128, 191, 255);;;;;
Gross profit;<bold>;rgb(0, 102, 204);<white>;;<center>;<large>;
Operating expenses;<bold>;;<italic>;;;;
Total selling expenses;<bold>;rgb(128, 191, 255);;;;;
General & administrative expenses;<bold>;;<italic>;;<center>;;<comment>
Other general & admin expenses;<bold>;rgb(128, 191, 255);<white>;;<center>;<large>;
total general & admin expenses;<bold>;<soft>;;;;;
total operating expenses;<bold>;rgb(128, 191, 255);;;;;
Operating income before taxes;<bold>;rgb(0, 102, 204);;<white>;;<large>;
Financial revenue & expenses;<bold>;;<italic>;;<center>;;<comment>
Income before tax & extraordinary items;<bold>;rgb(0, 102, 204);;<white>;;<large>;
Extraordinary items;<bold>;;<italic>;;<center>;;<comment>
Net gain on sale of land;<bold>;rgb(0, 102, 204);<white>;;<center>;<large>;
Extraordinary items after tax;<bold>;rgb(0, 102, 204);<white>;;<center>;<large>;
Net Income (Profit);<bold>;<night>;;<white>;<center>;<large>;
1 Accounts Bold Background FontStyle LetterColor Align Size Comment
2 Revenues <bold> <italic> <center> <comment>
3 Net sales revenues <bold> rgb(128, 191, 255)
4 Cost of goods sold <bold> rgb(128, 191, 255)
5 Manufacturing overhead <bold> <italic> <center> <comment>
6 Net mfr overhead <bold> rgb(128, 191, 255)
7 Net costs of goods sold <bold> rgb(128, 191, 255)
8 Gross profit <bold> rgb(0, 102, 204) <white> <center> <large>
9 Operating expenses <bold> <italic>
10 Total selling expenses <bold> rgb(128, 191, 255)
11 General & administrative expenses <bold> <italic> <center> <comment>
12 Other general & admin expenses <bold> rgb(128, 191, 255) <white> <center> <large>
13 total general & admin expenses <bold> <soft>
14 total operating expenses <bold> rgb(128, 191, 255)
15 Operating income before taxes <bold> rgb(0, 102, 204) <white> <large>
16 Financial revenue & expenses <bold> <italic> <center> <comment>
17 Income before tax & extraordinary items <bold> rgb(0, 102, 204) <white> <large>
18 Extraordinary items <bold> <italic> <center> <comment>
19 Net gain on sale of land <bold> rgb(0, 102, 204) <white> <center> <large>
20 Extraordinary items after tax <bold> rgb(0, 102, 204) <white> <center> <large>
21 Net Income (Profit) <bold> <night> <white> <center> <large>

Binary file not shown.

View File

@@ -1,21 +0,0 @@
Accounts;Bold;Background;FontStyle;LetterColor;Align;Size;Comment
Revenues;<bold>;;<italic>;;<center>;;<comment>
Net sales revenues;<bold>;RGB(225,226,226);;;;;
Cost of goods sold;<bold>;RGB(225,226,226);;;;;
Manufacturing overhead;<bold>;;<italic>;;<center>;;<comment>
Net mfr overhead;<bold>;RGB(225,226,226);;;;;
Net costs of goods sold;<bold>;RGB(225,226,226);;;;;
Gross profit;<bold>;RGB(193,216,47);;;<center>;<large>;
Operating expenses;<bold>;;<italic>;;;;
Total selling expenses;<bold>;RGB(225,226,226);;;;;
General & administrative expenses;<bold>;;<italic>;;<center>;;<comment>
Other general & admin expenses;<bold>;rgb(128, 191, 255);<white>;;<center>;<large>;
total general & admin expenses;<bold>;<soft>;;;;;
total operating expenses;<bold>;rgb(128, 191, 255);<white>;;;;
Operating income before taxes;<bold>;RGB(193,216,47);;;;<large>;
Financial revenue & expenses;<bold>;;<italic>;;<center>;;<comment>
Income before tax & extraordinary items;<bold>;RGB(193,216,47);;;;<large>;
Extraordinary items;<bold>;;<italic>;;<center>;;<comment>
Net gain on sale of land;<bold>;RGB(193,216,47);;;<center>;<large>;
Extraordinary items after tax;<bold>;RGB(193,216,47);;;<center>;<large>;
Net Income (Profit);<bold>;<night>;;<white>;<center>;<large>;
1 Accounts Bold Background FontStyle LetterColor Align Size Comment
2 Revenues <bold> <italic> <center> <comment>
3 Net sales revenues <bold> RGB(225,226,226)
4 Cost of goods sold <bold> RGB(225,226,226)
5 Manufacturing overhead <bold> <italic> <center> <comment>
6 Net mfr overhead <bold> RGB(225,226,226)
7 Net costs of goods sold <bold> RGB(225,226,226)
8 Gross profit <bold> RGB(193,216,47) <center> <large>
9 Operating expenses <bold> <italic>
10 Total selling expenses <bold> RGB(225,226,226)
11 General & administrative expenses <bold> <italic> <center> <comment>
12 Other general & admin expenses <bold> rgb(128, 191, 255) <white> <center> <large>
13 total general & admin expenses <bold> <soft>
14 total operating expenses <bold> rgb(128, 191, 255) <white>
15 Operating income before taxes <bold> RGB(193,216,47) <large>
16 Financial revenue & expenses <bold> <italic> <center> <comment>
17 Income before tax & extraordinary items <bold> RGB(193,216,47) <large>
18 Extraordinary items <bold> <italic> <center> <comment>
19 Net gain on sale of land <bold> RGB(193,216,47) <center> <large>
20 Extraordinary items after tax <bold> RGB(193,216,47) <center> <large>
21 Net Income (Profit) <bold> <night> <white> <center> <large>

View File

@@ -2,8 +2,8 @@
set -o errexit
echo "Creating release for version: $VERSION"
echo "Artifact name: ./build/${3}_${VERSION}.zip"
$HOME/bin/ghr -t ${ghoauth} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} ${VERSION} "./build/${3}_${4}.zip"
echo "Artifact name: ./dist/${3}_${VERSION}.zip"
$HOME/bin/ghr -t ${ghoauth} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} ${VERSION} "./dist/${3}_${4}.zip"
# Usage

25
scripts/verify-files.sh Normal file
View File

@@ -0,0 +1,25 @@
#!/bin/bash
# The build script has a known race-condition that sometimes causes it to not include all files
# in the built zip. This script verifies the that the zip contains the correct number of files.
set -o errexit
echo "Verifying built file count"
while read line; do
if [[ $line =~ ^\"name\": ]]; then
name=${line#*: \"}
name=${name%\"*}
fi
done < package.json
expected_file_count=$(($(find dist -type f | wc -l)-1))
zip_file_count=$(zipinfo dist/${name}_${VERSION}.zip | grep ^- | wc -l)
if [ "${expected_file_count}" -ne "${zip_file_count}" ]; then
# File count is incorrect
echo "Expected file count ${expected_file_count}, but was ${zip_file_count}"
exit 1
fi
echo "File count OK"
exit 0

View File

@@ -1,13 +0,0 @@
const path = require('path');
const packageJSON = require('./package.json');
const defaultBuildDestination = path.resolve("./build");
module.exports = {
buildDestination: process.env.BUILD_PATH || defaultBuildDestination,
mode: process.env.NODE_ENV || 'development',
name: packageJSON.name,
version: process.env.VERSION || 'local-dev',
url: process.env.BUILD_URL || defaultBuildDestination,
port: process.env.PORT || 8085
};

View File

@@ -0,0 +1,187 @@
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) {
super(props);
this.handleSelect = this.handleSelect.bind(this);
}
handleSelect () {
const { data: { meta: { dimensionCount } }, general: { allowFilteringByClick }, measurement, qlik } = this.props;
const hasSecondDimension = dimensionCount > 1;
if (!allowFilteringByClick) {
return;
}
qlik.backendApi.selectValues(0, [measurement.parents.dimension1.elementNumber], true);
if (hasSecondDimension) {
qlik.backendApi.selectValues(1, [measurement.parents.dimension2.elementNumber], true);
}
}
render () {
const {
data,
general,
measurement,
styleBuilder,
styling
} = this.props;
let textAlignment = styling.options.textAlignment || 'Right';
let cellStyle = {
fontFamily: styling.options.fontFamily,
...styleBuilder.getStyle(),
paddingLeft: '5px',
textAlign: textAlignment
};
const isEmptyCell = measurement.displayValue === '';
const isColumnPercentageBased = (/%/).test(measurement.format);
let formattedMeasurementValue;
if (isEmptyCell) {
formattedMeasurementValue = '';
cellStyle.cursor = 'default';
} else if (styleBuilder.hasComments()) {
formattedMeasurementValue = '.';
} else {
formattedMeasurementValue = formatMeasurementValue(measurement, styling);
}
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;
}
}
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';
}
return (
<td
className={`${cellClass}${general.cellSuffix}`}
onClick={isEmptyCell ? null : this.handleSelect}
style={cellStyle}
>
<Tooltip
styling={styling}
tooltipText={formattedMeasurementValue}
>
{formattedMeasurementValue}
</Tooltip>
</td>
);
}
}
DataCell.propTypes = {
data: PropTypes.shape({
headers: PropTypes.shape({
measurements: PropTypes.array.isRequired
}).isRequired
}).isRequired,
general: PropTypes.shape({
cellSuffix: PropTypes.string.isRequired
}).isRequired,
measurement: PropTypes.shape({
format: PropTypes.string,
name: PropTypes.string,
value: PropTypes.any
}).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,
styleBuilder: PropTypes.shape({
hasComments: PropTypes.func.isRequired
}).isRequired,
styling: PropTypes.shape({
symbolForNulls: PropTypes.any.isRequired
}).isRequired
};
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;
}
}
}
return formattedMeasurementValue;
}
function getConditionalColor (measurement, conditionalColoring) {
if (measurement.value < conditionalColoring.threshold.poor) {
return conditionalColoring.colors.poor;
}
if (measurement.value < conditionalColoring.threshold.fair) {
return conditionalColoring.colors.fair;
}
return conditionalColoring.colors.good;
}

View File

@@ -0,0 +1,28 @@
import React from 'react';
import PropTypes from 'prop-types';
const HeaderPadding = ({ styleBuilder, styling }) => {
if (styling.usePadding && !styleBuilder.hasCustomFileStyle()) {
const paddingStyle = {
fontFamily: styling.options.fontFamily,
marginLeft: '15px'
};
return (
<span style={paddingStyle} />
);
}
return null;
};
HeaderPadding.propTypes = {
styleBuilder: PropTypes.shape({
hasCustomFileStyle: PropTypes.func.isRequired
}).isRequired,
styling: PropTypes.shape({
options: PropTypes.shape({
fontFamily: PropTypes.string.isRequired
}).isRequired
}).isRequired
};
export default HeaderPadding;

117
src/data-table/index.jsx Normal file
View File

@@ -0,0 +1,117 @@
import React from 'react';
import PropTypes from 'prop-types';
import StyleBuilder from '../style-builder';
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;
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
entry={dimensionEntry}
qlik={qlik}
rowStyle={rowStyle}
styleBuilder={styleBuilder}
styling={styling}
/> : null
}
{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}
styleBuilder={styleBuilder}
styling={styling}
/>
);
})}
</tr>
);
})}
</tbody>
</table>
</div>
);
};
DataTable.defaultProps = {
renderData: true
};
DataTable.propTypes = {
data: PropTypes.shape({
headers: PropTypes.shape({
dimension1: PropTypes.array.isRequired
}).isRequired,
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
}).isRequired
};
export default DataTable;

View File

@@ -0,0 +1,63 @@
import React from 'react';
import PropTypes from 'prop-types';
import HeaderPadding from './header-padding.jsx';
import Tooltip from '../tooltip/index.jsx';
class RowHeader extends React.PureComponent {
constructor (props) {
super(props);
this.handleSelect = this.handleSelect.bind(this);
}
handleSelect () {
const { entry, qlik } = this.props;
qlik.backendApi.selectValues(0, [entry.elementNumber], true);
}
render () {
const { entry, rowStyle, styleBuilder, styling, qlik } = this.props;
const inEditState = qlik.inEditState();
return (
<td
className="fdim-cells"
onClick={this.handleSelect}
style={rowStyle}
>
<Tooltip
isTooltipActive={!inEditState}
styling={styling}
tooltipText={entry.displayValue}
>
<HeaderPadding
styleBuilder={styleBuilder}
styling={styling}
/>
{entry.displayValue}
</Tooltip>
</td>
);
}
}
RowHeader.propTypes = {
entry: PropTypes.shape({
displayValue: PropTypes.string.isRequired
}).isRequired,
qlik: PropTypes.shape({
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,
rowStyle: PropTypes.shape({}).isRequired,
styleBuilder: PropTypes.shape({}).isRequired,
styling: PropTypes.shape({}).isRequired
};
export default RowHeader;

58
src/dataset.js Normal file
View File

@@ -0,0 +1,58 @@
import qlik from 'qlik';
function createCube (definition, app) {
return new Promise(resolve => {
app.createCube(definition, resolve);
});
}
async function buildDataCube (originCubeDefinition, hasTwoDimensions, app) {
const cubeDefinition = {
...originCubeDefinition,
qInitialDataFetch: [
{
qHeight: 1000,
qWidth: 10
}
],
qDimensions: [originCubeDefinition.qDimensions[0]],
qMeasures: originCubeDefinition.qMeasures
};
if (hasTwoDimensions) {
cubeDefinition.qDimensions.push(originCubeDefinition.qDimensions[1]);
}
const cube = await createCube(cubeDefinition, app);
return cube.qHyperCube.qDataPages[0].qMatrix;
}
export async function initializeDataCube (component, layout) {
if (component.backendApi.isSnapshot) {
return layout.snapshotData.dataCube;
}
const app = qlik.currApp(component);
const hyperCubeDef = (await component.backendApi.getProperties()).qHyperCubeDef;
return buildDataCube(hyperCubeDef, layout.qHyperCube.qDimensionInfo.length === 2, app);
}
export function initializeDesignList (component, layout) {
if (component.backendApi.isSnapshot) {
return layout.snapshotData.designList;
}
if (!layout.stylingfield) {
return null;
}
return new Promise(resolve => {
const app = qlik.currApp(component);
const stylingField = app.field(layout.stylingfield);
const listener = function () {
const data = stylingField.rows.map(row => row.qText);
stylingField.OnData.unbind(listener);
resolve(data);
};
stylingField.OnData.bind(listener);
stylingField.getData();
});
}

View File

@@ -1,68 +0,0 @@
const colorLibrary = {
type: 'items',
label: 'Primary Colors Library',
items: {
ColLibClean: {
ref: 'collibclean',
translation: 'Clean',
type: 'string',
defaultValue: '#ffffff'
},
ColLibSoft: {
ref: 'collibsoft',
translation: 'Soft',
type: 'string',
defaultValue: '#efefef'
},
ColLibDark: {
ref: 'collibdark',
translation: 'Dark',
type: 'string',
defaultValue: '#c4c4c4'
},
ColLibNight: {
ref: 'collibnight',
translation: 'Night',
type: 'string',
defaultValue: '#808080'
},
ColLibRed: {
ref: 'collibred',
translation: 'Red',
type: 'string',
defaultValue: '#d58b94'
},
ColLibOrange: {
ref: 'colliborange',
translation: 'Orange',
type: 'string',
defaultValue: '#fd6600'
},
ColLibViolete: {
ref: 'collibviolete',
translation: 'Violete',
type: 'string',
defaultValue: '#ccc0ff'
},
ColLibBlue: {
ref: 'collibblue',
translation: 'Blue',
type: 'string',
defaultValue: '#4575b4'
},
ColLibGreen: {
ref: 'collibgreen',
translation: 'Green',
type: 'string',
defaultValue: '#7bb51c'
},
ColLibCustom: {
ref: 'collibcustom',
label: 'Custom',
type: 'string',
defaultValue: '#ffcccc'
}
}
};
export default colorLibrary;

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 condition',
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 by condition',
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: 'Measure indices (ex: 0,3)',
type: 'string',
defaultValue: '',
show (data) {
return data.conditionalcoloring.enabled
&& data.conditionalcoloring.colorallmeasures === false;
}
},
ThresholdPoor: {
ref: 'conditionalcoloring.threshold_poor',
translation: 'Poor is less than',
type: 'number',
defaultValue: -0.1,
show (data) {
return data.conditionalcoloring.enabled;
}
},
ColorPoor: {
ref: 'conditionalcoloring.color_poor',
label: 'Poor color fill',
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 is less than',
type: 'number',
defaultValue: 0,
show (data) {
return data.conditionalcoloring.enabled;
}
},
ColorFair: {
ref: 'conditionalcoloring.color_fair',
label: 'Fair color fill',
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 color fill',
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

@@ -24,102 +24,26 @@ const header = {
defaultValue: 2
},
headercolors: {
component: 'color-picker',
defaultValue: {
index: 6,
color: '#4477aa'
},
label: 'Background Header Color',
ref: 'HeaderColorSchema',
type: 'string',
component: 'dropdown',
label: 'BackGround Header Color',
options: [
{
value: 'Clean',
label: 'Clean'
},
{
value: 'Soft',
label: 'Soft'
},
{
value: 'Dark',
label: 'Dark'
},
{
value: 'Night',
label: 'Night'
},
{
value: 'Blue',
label: 'Blue'
},
{
value: 'Orange',
label: 'Orange'
},
{
value: 'Red',
label: 'Red'
},
{
value: 'Green',
label: 'Green'
},
{
value: 'Violete',
label: 'Violete'
},
{
value: 'Custom',
label: 'Custom'
}
],
defaultValue: 'Night'
type: 'object',
dualOutput: true
},
HeaderTextColor: {
ref: 'HeaderTextColorSchema',
type: 'string',
component: 'dropdown',
label: 'Text Header Color',
options: [
{
value: 'Black',
label: 'Black'
},
{
value: 'DimGray',
label: 'DimGray'
},
{
value: 'ForestGreen',
label: 'ForestGreen'
},
{
value: 'Gainsboro',
label: 'Gainsboro'
},
{
value: 'Indigo',
label: 'Indigo'
},
{
value: 'Navy',
label: 'Navy'
},
{
value: 'Purple',
label: 'Purple'
},
{
value: 'WhiteSmoke',
label: 'WhiteSmoke'
},
{
value: 'White',
label: 'White'
},
{
value: 'YellowGreen',
label: 'YellowGreen'
}
],
defaultValue: 'WhiteSmoke'
component: 'color-picker',
defaultValue: {
index: 1,
color: '#ffffff'
},
type: 'object',
dualOutput: true
},
HeaderFontSize: {
ref: 'lettersizeheader',
@@ -136,7 +60,7 @@ const header = {
label: 'Medium'
}
],
defaultValue: 2
defaultValue: 1
}
}
};

View File

@@ -1,38 +1,53 @@
import pagination from './pagination';
import header from './header';
import formatted from './formatted';
import conceptSemaphores from './concept-semaphores';
import metricSemaphores from './metric-semaphores';
import colorLibrary from './color-library';
import pijamaColorLibrary from './pijama-color-library';
import tableFormat from './table-format';
import conditionalColoring from './conditional-coloring';
const definition = {
component: 'accordion',
items: {
dimensions: {
max: 2,
min: 1,
uses: 'dimensions'
},
measures: {
max: 9,
min: 1,
uses: 'measures'
},
settings: {
data: {
items: {
ColorLibrary: colorLibrary,
ConceptSemaphores: conceptSemaphores,
Formatted: formatted,
Header: header,
MetricSemaphores: metricSemaphores,
Pagination: pagination,
PijamaColorLibrary: pijamaColorLibrary
dimensions: {
disabledRef: ''
},
measures: {
disabledRef: ''
}
},
uses: 'settings'
uses: 'data'
},
sorting: {
uses: 'sorting'
},
settings: {
items: {
Formatted: tableFormat,
Header: header,
ConditionalColoring: conditionalColoring,
Pagination: pagination
},
uses: 'settings'
},
about: {
component: 'items',
label: 'About',
items: {
header: {
label: 'P&L pivot',
style: 'header',
component: 'text'
},
paragraph1: {
label: `P&L pivot is a Qlik Sense extension which allows you to display Profit & Loss
reporting with color and font customizations.`,
component: 'text'
},
paragraph2: {
label: 'P&L pivot is based upon an extension created by Ivan Felipe Asensio.',
component: 'text'
}
}
}
},
type: 'items'

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: '0',
show (data) {
return data.allmetrics == false;
}
},
MetricStatus1: {
ref: 'metricsstatus1',
translation: 'Critic is less than',
type: 'number',
defaultValue: -0.1
},
ColorStatus1: {
ref: 'colorstatus1',
label: 'Critic Color Fill',
type: 'object',
component: 'color-picker',
defaultValue: {
index: 7,
color: '#f93f17'
}
},
ColorStatus1Text: {
ref: 'colorstatus1text',
label: 'Critic Color Text',
type: 'object',
component: 'color-picker',
defaultValue: {
index: 10,
color: '#ffffff'
}
},
MetricStatus2: {
ref: 'metricsstatus2',
translation: 'Medium is less than',
type: 'number',
defaultValue: 0
},
ColorStatus2: {
ref: 'colorstatus2',
label: 'Medium Color Fill',
type: 'object',
component: 'color-picker',
defaultValue: {
index: 8,
color: '#ffcf02'
}
},
ColorStatus2Text: {
ref: 'colorstatus2text',
label: 'Medium Color Text',
type: 'object',
component: 'color-picker',
defaultValue: {
index: 11,
color: '#000000'
}
},
ColorStatus3: {
ref: 'colorstatus3',
label: 'Success Color Fill',
type: 'object',
component: 'color-picker',
defaultValue: {
index: 9,
color: '#276e27'
}
},
ColorStatus3Text: {
ref: 'colorstatus3text',
label: 'Success Color Text',
type: 'object',
component: 'color-picker',
defaultValue: {
index: 10,
color: '#ffffff'
}
}
}
};
export default metricSemaphores;

View File

@@ -55,7 +55,7 @@ const pagination = {
ref: 'errormessage',
label: 'Default error message',
type: 'string',
defaultValue: 'Ups! It seems you asked for too many data. Please filter more to see the whole picture.'
defaultValue: 'Unable to display all the data. Apply more filters to limit the amount of displayed data.'
}
}
};

View File

@@ -1,68 +0,0 @@
const pijamaColorLibrary = {
type: 'items',
label: 'Pijama Colors Library',
items: {
ColLibCleanP: {
ref: 'collibcleanp',
translation: 'Clean',
type: 'string',
defaultValue: '#ffffff'
},
ColLibSoftP: {
ref: 'collibsoftp',
translation: 'Soft',
type: 'string',
defaultValue: '#ffffff'
},
ColLibDarkP: {
ref: 'collibdarkp',
translation: 'Dark',
type: 'string',
defaultValue: '#efefef'
},
ColLibNightP: {
ref: 'collibnightp',
translation: 'Night',
type: 'string',
defaultValue: '#c4c4c4'
},
ColLibRedP: {
ref: 'collibredp',
translation: 'Red',
type: 'string',
defaultValue: '#ffcccc'
},
ColLibOrangeP: {
ref: 'colliborangep',
translation: 'Orange',
type: 'string',
defaultValue: '#ffcc66'
},
ColLibVioleteP: {
ref: 'collibvioletep',
translation: 'Violete',
type: 'string',
defaultValue: '#e6e6ff'
},
ColLibBlueP: {
ref: 'collibbluep',
translation: 'Blue',
type: 'string',
defaultValue: '#b3d9ff'
},
ColLibGreenP: {
ref: 'collibgreenp',
translation: 'Green',
type: 'string',
defaultValue: '#98fb98'
},
ColLibCustomP: {
ref: 'collibcustomp',
label: 'Custom',
type: 'string',
defaultValue: '#ffffff'
}
}
};
export default pijamaColorLibrary;

View File

@@ -1,12 +1,59 @@
const formatted = {
const qlik = window.require('qlik');
// fixes case for when there are 3 dimensions, missies the case with 1 design dimension and 1 data dimension
function hasDesignDimension (data) {
return data.qHyperCubeDef.qDimensions.length > 2;
}
function getFieldList () {
return new Promise(function (resolve) {
const app = qlik.currApp();
app.getList('FieldList').then(function (model) {
// Close the model to prevent any updates.
app.destroySessionObject(model.layout.qInfo.qId);
// This is a bit iffy, might be smarter to reject and handle empty lists on the props instead.
if (!model.layout.qFieldList.qItems) {
return resolve([]);
}
// Resolve an array with master objects.
return resolve(model.layout.qFieldList.qItems.map(function (item) {
return {
value: item.qName,
label: item.qName
};
}));
});
});
}
const tableFormat = {
type: 'items',
label: 'Table Format',
items: {
StylingField: {
ref: 'stylingfield',
disabledRef: '',
type: 'string',
component: 'dropdown',
label: 'Style with field',
options: function () {
return getFieldList().then(function (items) {
items.unshift(
{
value: '',
label: 'None'
});
return items;
});
}
},
IndentBool: {
ref: 'indentbool',
type: 'boolean',
label: 'Indent',
defaultValue: true
defaultValue: false,
show: data => !hasDesignDimension(data)
},
SeparatorColumns: {
ref: 'separatorcols',
@@ -14,72 +61,29 @@ const formatted = {
label: 'Separator Columns',
defaultValue: false
},
CustomFileBool: {
ref: 'customfilebool',
type: 'boolean',
label: 'Include External File',
defaultValue: false
rowEvenBGColor: {
component: 'color-picker',
defaultValue: {
color: '#fff',
index: 1
},
label: 'Even row background color',
ref: 'rowEvenBGColor',
type: 'object',
dualOutput: true,
show: data => !hasDesignDimension(data)
},
CustomFile: {
ref: 'customfile',
label: 'Name of CSV file (; separated)',
type: 'string',
defaultValue: '',
show (data) {
return data.customfilebool;
}
},
colors: {
ref: 'ColorSchema',
type: 'string',
component: 'dropdown',
label: 'BackGround Style',
options: [
{
value: 'Clean',
label: 'Clean'
},
{
value: 'Soft',
label: 'Soft'
},
{
value: 'Dark',
label: 'Dark'
},
{
value: 'Night',
label: 'Night'
},
{
value: 'Blue',
label: 'Blue'
},
{
value: 'Orange',
label: 'Orange'
},
{
value: 'Red',
label: 'Red'
},
{
value: 'Green',
label: 'Green'
},
{
value: 'Violete',
label: 'Violete'
},
{
value: 'Custom',
label: 'Custom'
}
],
defaultValue: 'Clean',
show (data) {
return data.customfilebool == false;
}
rowOddBGColor: {
component: 'color-picker',
defaultValue: {
color: '#b6d7ea',
index: 4
},
label: 'Odd row background color',
ref: 'rowOddBGColor',
type: 'object',
dualOutput: true,
show: data => !hasDesignDimension(data)
},
BodyTextColor: {
ref: 'BodyTextColorSchema',
@@ -129,9 +133,7 @@ const formatted = {
}
],
defaultValue: 'Black',
show (data) {
return data.customfilebool == false;
}
show: data => !hasDesignDimension(data)
},
FontFamily: {
ref: 'FontFamily',
@@ -140,31 +142,35 @@ const formatted = {
label: 'FontFamily',
options: [
{
value: 'Arial',
value: 'QlikView Sans, -apple-system, sans-serif',
label: 'QlikView Sans'
},
{
value: 'Arial, -apple-system, sans-serif',
label: 'Arial'
},
{
value: 'Calibri',
value: 'Calibri, -apple-system, sans-serif',
label: 'Calibri'
},
{
value: 'Comic Sans MS',
value: 'Comic Sans MS, -apple-system, sans-serif',
label: 'Comic Sans MS'
},
{
value: 'MS Sans Serif',
value: 'MS Sans Serif, -apple-system, sans-serif',
label: 'MS Sans Serif'
},
{
value: 'Tahoma',
value: 'Tahoma, -apple-system, sans-serif',
label: 'Tahoma'
},
{
value: 'Verdana',
value: 'Verdana, -apple-system, sans-serif',
label: 'Verdana'
}
],
defaultValue: 'Calibri'
defaultValue: 'QlikView Sans, -apple-system, sans-serif'
},
DataFontSize: {
ref: 'lettersize',
@@ -181,7 +187,27 @@ const formatted = {
label: 'Medium'
}
],
defaultValue: 2
defaultValue: 1
},
textAlignment: {
ref: 'cellTextAlignment',
label: 'Cell Text alignment',
component: 'buttongroup',
options: [
{
value: 'left',
label: 'Left'
},
{
value: 'center',
label: 'Center'
},
{
value: 'right',
label: 'Right'
}
],
defaultValue: 'right'
},
ColumnWidthSlider: {
type: 'number',
@@ -236,4 +262,4 @@ const formatted = {
}
};
export default formatted;
export default tableFormat;

View File

@@ -1,222 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ApplyPreMask } from './masking';
import { addSeparators } from './utilities';
class ElseDimensionMeasures extends React.PureComponent {
render () {
const {
vFontFamily,
vSeparatorCols,
measure_count,
sufixCells,
vSymbolForNulls,
vLetterSize,
vColorMetric1,
vColorMetric1Text,
vColorMetric2,
vColorMetric2Text,
vColorMetric3,
vColorMetric3Text,
vAllSemaphores,
ConceptMatrixPivot,
ConceptsAffectedMatrix,
vAllMetrics,
MetricsAffectedMatrix,
vCritic,
vMMedium,
vNumMeasures,
vNumMeasures2,
MeasuresFormat,
rowNumber,
columnText,
styleBuilder
} = this.props;
// modified in here
let columnNumber,
vMaskNum,
vColorSemaphore,
vColorSemaphoreText,
vDivide;
const measurementCells = [];
var nMeasure7 = 0;
var nMeasure72 = -1;
var nMeasure72Semaphore = 0;
for (var nMeasures22 = 1; nMeasures22 <= vNumMeasures2; nMeasures22++) {
nMeasure7++;
nMeasure72++;
if (columnText.substring(0, 1) === '%') {
columnNumber = ApplyPreMask('0,00%', ConceptMatrixPivot[rowNumber][nMeasures22]);
var vSpecialF = '0,00%';
} else {
switch (MeasuresFormat[nMeasure72].substr(MeasuresFormat[nMeasure72].length - 1)) {
case 'k':
vDivide = 1000;
break;
case 'K':
vDivide = 1000;
break;
case 'm':
vDivide = 1000000;
break;
case 'M':
vDivide = 1000000;
break;
default:
vDivide = 1;
break;
}
var vSpecialF = MeasuresFormat[nMeasure72].replace(/k|K|m|M/gi, '');
if (!isNaN(ConceptMatrixPivot[rowNumber][nMeasures22])) {
vMaskNum = ConceptMatrixPivot[rowNumber][nMeasures22];
if (vSpecialF.substring(vSpecialF.length - 1) === '%') {
vMaskNum = vMaskNum * 100;
}
switch (vSpecialF) {
case '#.##0':
columnNumber = addSeparators((vMaskNum / vDivide), '.', ',', 0);
break;
case '#,##0':
columnNumber = addSeparators((vMaskNum / vDivide), ',', '.', 0);
break;
default:
columnNumber = ApplyPreMask(vSpecialF, (vMaskNum / vDivide));
break;
}
} else {
columnNumber = vSymbolForNulls;
}
}
if (vSeparatorCols && nMeasure7 === (measure_count + 1)) {
const seperatorStyle = {
color: 'white',
fontFamily: vFontFamily,
fontSize: (12 + vLetterSize) + 'px'
};
const seperatorElement = (
<th key={`${nMeasures22}-header`} className="empty" style={seperatorStyle}>
*
</th>
);
measurementCells.push(seperatorElement);
nMeasure7 = 1;
}
if (nMeasure72 === (measure_count - 1)) {
nMeasure72 = -1;
nMeasure72Semaphore = measure_count;
} else {
nMeasure72Semaphore = nMeasure72 + 1;
}
// apply the semaphores where needed
if (styleBuilder.hasComments()) {
columnNumber = '.';
}
let cellElement;
if ((vAllSemaphores || ConceptsAffectedMatrix.indexOf(columnText) >= 0) && (vAllMetrics || MetricsAffectedMatrix.indexOf(nMeasure72Semaphore) >= 0) && !isNaN(ConceptMatrixPivot[rowNumber][nMeasures22]) && !styleBuilder.hasComments()) {
if (ConceptMatrixPivot[rowNumber][nMeasures22] < vCritic) {
vColorSemaphore = vColorMetric1;
vColorSemaphoreText = vColorMetric1Text;
} else {
if (ConceptMatrixPivot[rowNumber][nMeasures22] < vMMedium) {
vColorSemaphore = vColorMetric2;
vColorSemaphoreText = vColorMetric2Text;
} else {
vColorSemaphore = vColorMetric3;
vColorSemaphoreText = vColorMetric3Text;
}
}
const cellStyle = {
fontFamily: vFontFamily,
fontSize: styleBuilder.getStyle().fontSize,
color: vColorSemaphoreText,
backgroundColor: vColorSemaphore,
textAlign: 'right',
paddingLeft: '4px'
};
if (vSpecialF.substring(vSpecialF.length - 1) === '%' && vNumMeasures > 1) {
cellElement = (
<td key={nMeasures22} className={'grid-cells-small' + sufixCells} style={cellStyle}>
{columnNumber}
</td>
);
} else {
cellElement = (
<td key={nMeasures22} className={'grid-cells' + sufixCells} style={cellStyle}>
{columnNumber}
</td>
);
}
} else {
const cellStyle = {
fontFamily: vFontFamily,
...styleBuilder.getStyle(),
textAlign: 'right',
paddingRight: '4px'
};
if (vSpecialF.substring(vSpecialF.length - 1) === '%' && vNumMeasures > 1) {
cellElement = (
<td key={nMeasures22} className={'grid-cells-small' + sufixCells} style={cellStyle}>
{columnNumber}
</td>
);
} else {
cellElement = (
<td key={nMeasures22} className={'grid-cells' + sufixCells} style={cellStyle}>
{columnNumber}
</td>
);
}
}
measurementCells.push(cellElement);
}
return (
<React.Fragment>
{measurementCells}
</React.Fragment>
);
}
}
ElseDimensionMeasures.propTypes = {
vFontFamily: PropTypes.any,
vSeparatorCols: PropTypes.any,
measure_count: PropTypes.any,
sufixCells: PropTypes.any,
vSymbolForNulls: PropTypes.any,
vLetterSize: PropTypes.any,
vColorMetric1: PropTypes.any,
vColorMetric1Text: PropTypes.any,
vColorMetric2: PropTypes.any,
vColorMetric2Text: PropTypes.any,
vColorMetric3: PropTypes.any,
vColorMetric3Text: PropTypes.any,
vAllSemaphores: PropTypes.any,
ConceptMatrixPivot: PropTypes.any,
ConceptsAffectedMatrix: PropTypes.any,
vAllMetrics: PropTypes.any,
MetricsAffectedMatrix: PropTypes.any,
vCritic: PropTypes.any,
vMMedium: PropTypes.any,
vNumMeasures: PropTypes.any,
vNumMeasures2: PropTypes.any,
MeasuresFormat: PropTypes.any,
rowNumber: PropTypes.any,
columnText: PropTypes.any,
styleBuilder: PropTypes.any
};
export default ElseDimensionMeasures;

View File

@@ -1,73 +1,107 @@
import $ from 'jquery';
const isIE = /* @cc_on!@*/false || Boolean(document.documentMode);
const isChrome = Boolean(window.chrome) && Boolean(window.chrome.webstore);
const isSafari = Object.prototype.toString.call(window.HTMLElement).indexOf('Constructor') > 0;
const isFirefox = typeof InstallTrigger !== 'undefined';
export function enableExcelExport (layout, f) {
let myTitle = '';
let mySubTitle = '';
let myFootNote = '';
if (layout.title.length > 0) {
myTitle += '<p style="font-size:15pt"><b>';
myTitle += layout.title;
myTitle += '</b></p>';
}
if (layout.subtitle.length > 0) {
mySubTitle += '<p style="font-size:11pt">';
mySubTitle += layout.subtitle;
mySubTitle += '</p>';
}
if (layout.footnote.length > 0) {
myFootNote += '<p style="font-size:11pt"><i>Note:</i>';
myFootNote += layout.footnote;
myFootNote += '</p>';
}
$('.icon-xls').on('click', () => {
$('.header-wrapper th').children('.tooltip')
.remove(); // remove some popup effects when exporting
$('.header-wrapper th').children('.icon-xls')
.remove(); // remove the xls icon when exporting
if (isChrome || isSafari) {
const $clonedDiv = $('.data-table').clone(true); // .kpi-table a secas exporta la 1ªcol
let vEncodeHead = '<html><head><meta charset="UTF-8"></head>';
vEncodeHead += myTitle + mySubTitle + myFootNote;
const vEncode = encodeURIComponent($clonedDiv.html());
let vDecode = `${vEncodeHead + vEncode}</html>`;
$clonedDiv.find('tr.header');
vDecode = vDecode.split('%3E.%3C').join('%3E%3C');
window.open(`data:application/vnd.ms-excel,${vDecode}`);
$.preventDefault();
}
if (isIE) {
let a = '<html><head><meta charset="UTF-8"></head>';
a += myTitle + mySubTitle + myFootNote;
a += f;
a = a.split('>.<').join('><');
a += '</html>';
const w = window.open();
w.document.open();
w.document.write(a);
w.document.close();
w.document.execCommand('SaveAs', true, 'Analysis.xls' || 'c:\TMP');
w.close();
}
if (isFirefox) {
const $clonedDiv = $('.data-table').clone(true);// .kpi-table a secas exporta la 1ªcol
let vEncodeHead = '<html><head><meta charset="UTF-8"></head>';
vEncodeHead += myTitle + mySubTitle + myFootNote;
const vEncode = encodeURIComponent($clonedDiv.html());
let vDecode = `${vEncodeHead + vEncode}</html>`;
$clonedDiv.find('tr.header');
vDecode = vDecode.split('>.<').join('><');
window.open(`data:application/vnd.ms-excel,${vDecode}`);
$.preventDefault();
function cleanupNodes (node) {
const removables = node.querySelectorAll('.tooltip,input');
[].forEach.call(removables, removeable => {
if (removeable.parentNode) {
removeable.parentNode.removeChild(removeable);
}
});
}
function buildTableHTML (id, 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">${footnote}</p>`;
const container = document.querySelector(`[tid="${id}"]`);
const kpiTableClone = container.querySelector('.kpi-table').cloneNode(true);
const dataTableClone = container.querySelector('.data-table').cloneNode(true);
cleanupNodes(kpiTableClone);
cleanupNodes(kpiTableClone);
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
xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:x="urn:schemas-microsoft-com:office:excel"
xmlns="http://www.w3.org/TR/REC-html40"
>
<head>
<meta charset="UTF-8">
<!--[if gte mso 9]>
<xml>
<x:ExcelWorkbook>
<x:ExcelWorksheets>
<x:ExcelWorksheet>
<x:Name>${title || 'Analyze'}</x:Name>
<x:WorksheetOptions>
<x:DisplayGridlines/>
</x:WorksheetOptions>
</x:ExcelWorksheet>
</x:ExcelWorksheets>
</x:ExcelWorkbook>
</xml>
<![endif]-->
</head>
<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 : ''}
</body>
</html>
`.split('>.<')
.join('><')
.split('>*<')
.join('><');
return tableHTML;
}
function downloadXLS (html) {
const filename = 'analysis.xls';
const blobObject = new Blob([html]);
// IE/Edge
if (window.navigator.msSaveOrOpenBlob) {
return window.navigator.msSaveOrOpenBlob(blobObject, filename);
}
const link = window.document.createElement('a');
link.href = URL.createObjectURL(blobObject);
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
return true;
}
export function exportXLS (id, 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(id, title, subtitle, footnote);
downloadXLS(table);
}

View File

@@ -1,13 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
import { exportXLS } from './excel-export';
// TODO: move interaction logic in here from excel-export.js
class ExportButton extends React.PureComponent {
constructor (props) {
super(props);
this.handleExport = this.handleExport.bind(this);
}
handleExport () {
const { id, excelExport, general } = this.props;
const { title, subtitle, footnote } = general;
if (excelExport) {
exportXLS(id, 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"
/>
@@ -20,7 +34,9 @@ ExportButton.defaultProps = {
};
ExportButton.propTypes = {
excelExport: PropTypes.bool
id: PropTypes.string.isRequired,
excelExport: PropTypes.bool,
general: PropTypes.shape({}).isRequired
};
export default ExportButton;

View File

@@ -1,9 +0,0 @@
/* https://randomhaiku.com */
describe('behind the money', () => {
describe('Canada and Panda work.', () => {
it('Tiger starts blowing.', () => {
expect(true).toBeTruthy();
});
});
});

View File

@@ -1,360 +0,0 @@
import React, { PureComponent, Fragment } from 'react';
import PropTypes from 'prop-types';
import ExportButton from './export-button.jsx';
/* TODO: the different render methods are very similar, split it into a few components
and try to get rid of some duplication */
class HeaderWrapper extends PureComponent {
getBaseCSS () {
const {
vHeaderColorText,
vFontFamily,
vHeaderColorSchema,
vHeaderAlignText
} = this.props;
const baseCSS = {
backgroundColor: vHeaderColorSchema,
color: vHeaderColorText,
fontFamily: vFontFamily,
textAlign: vHeaderAlignText
};
return baseCSS;
}
renderSecondDimensionTitles () {
const {
vFontFamily,
vLetterSizeHeader,
vExportToExcel,
sufixCells,
vNumDims,
measure_count,
LabelsArray,
SecondHeader,
vSeparatorCols,
nSecond,
vLetterSize,
ExtraLabelsArray
} = this.props;
const baseCSS = this.getBaseCSS();
if (vNumDims === 2) {
if (measure_count > 1) {
const thStyle = {
...baseCSS,
cursor: 'default',
fontSize: `${16 + vLetterSizeHeader} px`,
height: '80px',
verticalAlign: 'middle',
width: '230px'
};
return (
<Fragment key="second-dimension-titles">
<th
className="fdim-cells"
padding-left="20px"
rowSpan="2"
style={thStyle}
>
<ExportButton excelExport={vExportToExcel} />
{LabelsArray[0]}
</th>
{SecondHeader.map((header, index) => {
const emptyStyle = {
color: 'white',
fontFamily: vFontFamily,
fontSize: `${13 + vLetterSizeHeader} px`
};
const style = {
...baseCSS,
fontSize: `${14 + vLetterSizeHeader} px`,
height: '45px',
verticalAlign: 'middle'
};
return (
<Fragment key={index}>
{vSeparatorCols && index > 0 && (
<th
className="empty"
style={emptyStyle}
>
*
</th>
)}
<th
className={`grid-cells2${sufixCells}`}
colSpan={measure_count}
style={style}
>
{header}
</th>
</Fragment>
);
})}
</Fragment>
);
}
const fDimCellsStyle = {
...baseCSS,
cursor: 'default',
fontSize: `${16 + vLetterSizeHeader} px`,
height: '70px',
verticalAlign: 'middle',
width: '230px'
};
return (
<Fragment>
<th
className="fdim-cells"
style={fDimCellsStyle}
>
<ExportButton excelExport={vExportToExcel} />
{LabelsArray[0] + ExtraLabelsArray[0]}
</th>
{SecondHeader.map((entry, entryIndex) => {
// TODO: seperator element is reused a bunch, only difference being font-size
const hasSeperator = vSeparatorCols && nSecond > 0;
const seperatorStyle = {
color: 'white',
fontFamily: vFontFamily,
fontSize: `${12 + vLetterSize} px`
};
const seperatorElement = (
<th
className="empty"
style={seperatorStyle}
>
*
</th>
);
let sufixWrap = '';
if ((entry.length > 11 && vLetterSizeHeader === 0) || (entry.length > 12 && vLetterSizeHeader === -2)) {
sufixWrap = '70';
} else {
sufixWrap = 'Empty';
}
const gridCells2Style = {
...baseCSS,
fontSize: `${14 + vLetterSizeHeader} px`,
height: '70px',
verticalAlign: 'middle'
};
const wrapStyle = {
fontFamily: vFontFamily
};
return (
<Fragment key={entryIndex}>
{hasSeperator && seperatorElement}
<th
className={`grid-cells2${sufixCells}`}
style={gridCells2Style}
>
<span
className={`wrapclass${sufixWrap}`}
style={wrapStyle}
>
{entry}
</span>
</th>
</Fragment>
);
})}
</Fragment>
);
}
}
renderSecondDimensionSubTitles () {
const {
vFontFamily,
vLetterSizeHeader,
sufixCells,
LabelsArray,
SecondHeader,
vSeparatorCols,
vLetterSize,
MeasuresFormat,
ExtraLabelsArray
} = this.props;
const baseCSS = this.getBaseCSS();
return SecondHeader.map((header, index) => {
const emptyStyle = {
color: 'white',
fontFamily: vFontFamily,
fontSize: `${12 + vLetterSizeHeader} px`
};
return (
<Fragment key={index}>
{vSeparatorCols && index > 0 && (
<th
className="empty"
style={emptyStyle}
>
*
</th>
)}
{MeasuresFormat.map((measureFormat, measureFormatIndex) => {
if (measureFormat.substring(measureFormat.length - 1) === '%') {
const cells2SmallStyle = {
...baseCSS,
cursor: 'default',
fontSize: `${13 + vLetterSizeHeader} px`,
height: '25px',
verticalAlign: 'middle'
};
return (
<th key={measureFormatIndex} className={'grid-cells2-small' + sufixCells} style={cells2SmallStyle}>
<span className="wrapclass25">
{LabelsArray[measureFormatIndex + 1]}
{ExtraLabelsArray[measureFormatIndex]}
</span>
</th>
);
}
const cells2Style = {
...baseCSS,
cursor: 'default',
fontSize: `${14 + vLetterSizeHeader} px`,
height: '25px',
verticalAlign: 'middle'
};
return (
<th key={measureFormatIndex} className={'grid-cells2' + sufixCells} style={cells2Style}>
<span className="wrapclass25">
{LabelsArray[measureFormatIndex + 1]}
{ExtraLabelsArray[measureFormatIndex]}
</span>
</th>
);
})}
</Fragment>
);
});
}
renderMeasureInfos () {
const {
vFontFamily,
vLetterSizeHeader,
dim_count,
vExtraLabel,
sufixCells,
measureInfos
} = this.props;
const baseCSS = this.getBaseCSS();
if (dim_count === 1) {
return measureInfos.map((measureInfo, measureInfoIndex) => {
let sufixWrap = '';
if (((measureInfo.qFallbackTitle + vExtraLabel).length > 11 && vLetterSizeHeader === 0)
|| ((measureInfo.qFallbackTitle + vExtraLabel).length > 12 && vLetterSizeHeader === -2)) {
sufixWrap = '70';
} else {
sufixWrap = 'Empty';
}
const thStyle = {
...baseCSS,
cursor: 'default',
fontSize: `${15 + vLetterSizeHeader} px`,
height: '70px',
verticalAlign: 'middle'
};
return (
<th key={measureInfoIndex} className={'grid-cells2' + sufixCells} style={thStyle}>
<span className={'wrapclass' + sufixWrap} style={{ fontFamily: vFontFamily }}>
{measureInfo.qFallbackTitle + vExtraLabel}
</span>
</th>
);
});
}
return null;
}
renderDimensionInfos () {
const {
dimensionInfos,
vLetterSizeHeader,
vExportToExcel
} = this.props;
const baseCSS = this.getBaseCSS();
return dimensionInfos.map((dimensionInfo, dimensionInfoIndex) => {
// TODO: move static properties to css file
const style = {
...baseCSS,
cursor: 'default',
fontSize: `${17 + vLetterSizeHeader} px`,
height: '70px',
verticalAlign: 'middle',
width: '230px'
};
return (
<th
className="fdim-cells"
key={dimensionInfoIndex}
style={style}
>
<ExportButton excelExport={vExportToExcel} />
{dimensionInfo.qFallbackTitle}
</th>
);
});
}
render () {
const { vNumDims, measure_count } = this.props;
return (
<div className="header-wrapper">
<table className="header">
<tbody>
<tr>
{this.renderDimensionInfos()}
{this.renderMeasureInfos()}
{this.renderSecondDimensionTitles()}
</tr>
{ vNumDims === 2 && measure_count > 1 && (
<tr>
{this.renderSecondDimensionSubTitles()}
</tr>
)}
</tbody>
</table>
</div>
);
}
}
// TODO: make any, object and array forbidden
HeaderWrapper.propTypes = {
vHeaderColorText: PropTypes.any,
vFontFamily: PropTypes.any,
vHeaderColorSchema: PropTypes.any,
vExportToExcel: PropTypes.any,
vNumDims: PropTypes.any,
dimensionInfos: PropTypes.any,
vLetterSizeHeader: PropTypes.any,
vHeaderAlignText: PropTypes.any,
MeasuresFormat: PropTypes.any,
measure_count: PropTypes.any,
sufixCells: PropTypes.any,
LabelsArray: PropTypes.any,
SecondHeader: PropTypes.any,
vSeparatorCols: PropTypes.any,
nSecond: PropTypes.any,
vLetterSize: PropTypes.any,
ExtraLabelsArray: PropTypes.any,
dim_count: PropTypes.any,
vExtraLabel: PropTypes.any,
measureInfos: PropTypes.any
};
export default HeaderWrapper;

View File

@@ -0,0 +1,78 @@
import React from 'react';
import PropTypes from 'prop-types';
import { HEADER_FONT_SIZE } from '../initialize-transformed';
import Tooltip from '../tooltip/index.jsx';
class ColumnHeader extends React.PureComponent {
constructor (props) {
super(props);
this.handleSelect = this.handleSelect.bind(this);
}
handleSelect () {
const { entry, qlik } = this.props;
qlik.backendApi.selectValues(1, [entry.elementNumber], true);
}
render () {
const { baseCSS, cellSuffix, colSpan, entry, styling, qlik } = this.props;
const inEditState = qlik.inEditState();
const isMediumFontSize = styling.headerOptions.fontSizeAdjustment === HEADER_FONT_SIZE.MEDIUM;
const style = {
...baseCSS,
fontSize: `${14 + styling.headerOptions.fontSizeAdjustment}px`,
height: isMediumFontSize ? '43px' : '33px',
verticalAlign: 'middle'
};
return (
<th
className={`grid-cells2${cellSuffix}`}
colSpan={colSpan}
onClick={this.handleSelect}
style={style}
>
<Tooltip
isTooltipActive={!inEditState}
styling={styling}
tooltipText={entry.displayValue}
>
{entry.displayValue}
</Tooltip>
</th>
);
}
}
ColumnHeader.defaultProps = {
cellSuffix: '',
colSpan: 1
};
ColumnHeader.propTypes = {
baseCSS: PropTypes.shape({}).isRequired,
cellSuffix: PropTypes.string,
colSpan: PropTypes.number,
entry: PropTypes.shape({
elementNumber: PropTypes.number.isRequired,
name: PropTypes.string.isRequired
}).isRequired,
qlik: PropTypes.shape({
backendApi: PropTypes.shape({
selectValues: function (props, propName) {
if (props.isSnapshot || typeof props[propName] === 'function') {
return null;
}
return new Error('Missing implementation of qlik.backendApi.selectValues.');
}
}).isRequired
}).isRequired,
styling: PropTypes.shape({
headerOptions: PropTypes.shape({
fontSizeAdjustment: PropTypes.number.isRequired
}).isRequired
}).isRequired
};
export default ColumnHeader;

View File

@@ -0,0 +1,48 @@
import React from 'react';
import PropTypes from 'prop-types';
import ExportButton from '../export-button.jsx';
import { HEADER_FONT_SIZE } from '../initialize-transformed';
const ExportColumnHeader = ({ id, baseCSS, general, title, allowExcelExport, hasSecondDimension, styling }) => {
const rowSpan = hasSecondDimension ? 2 : 1;
const isMediumFontSize = styling.headerOptions.fontSizeAdjustment === HEADER_FONT_SIZE.MEDIUM;
const style = {
...baseCSS,
cursor: 'default',
fontSize: `${16 + styling.headerOptions.fontSizeAdjustment}px`,
height: isMediumFontSize ? '90px' : '70px',
verticalAlign: 'middle',
width: '230px'
};
return (
<th
className="fdim-cells"
rowSpan={rowSpan}
style={style}
>
<ExportButton
id={id}
excelExport={allowExcelExport}
general={general}
/>
{title}
</th>
);
};
ExportColumnHeader.propTypes = {
id: PropTypes.string.isRequired,
allowExcelExport: PropTypes.bool.isRequired,
baseCSS: PropTypes.shape({}).isRequired,
general: PropTypes.shape({}).isRequired,
hasSecondDimension: PropTypes.bool.isRequired,
styling: PropTypes.shape({
headerOptions: PropTypes.shape({
fontSizeAdjustment: PropTypes.number.isRequired
}).isRequired
}).isRequired,
title: PropTypes.string.isRequired
};
export default ExportColumnHeader;

View File

@@ -0,0 +1,5 @@
function Model (component) {
this.component = component;
}
export default Model;

147
src/headers-table/index.jsx Normal file
View File

@@ -0,0 +1,147 @@
import React from 'react';
import PropTypes from 'prop-types';
import ExportColumnHeader from './export-column-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, isKpi }) => {
const baseCSS = {
backgroundColor: styling.headerOptions.colorSchema,
color: styling.headerOptions.textColor,
fontFamily: styling.options.fontFamily,
textAlign: styling.headerOptions.alignment
};
const {
dimension1,
dimension2,
measurements
} = data.headers;
const hasSecondDimension = dimension2.length > 0;
return (
<div className="header-wrapper">
<table className="header">
<tbody>
<tr>
{isKpi ?
<ExportColumnHeader
id={qlik.options.id}
allowExcelExport={general.allowExcelExport}
baseCSS={baseCSS}
general={general}
hasSecondDimension={hasSecondDimension}
styling={styling}
title={dimension1[0].name}
/> : null
}
{!isKpi && !hasSecondDimension && measurements.map(measurementEntry => (
<MeasurementColumnHeader
baseCSS={baseCSS}
general={general}
hasSecondDimension={hasSecondDimension}
key={`${measurementEntry.displayValue}-${measurementEntry.name}`}
measurement={measurementEntry}
styling={styling}
/>
))}
{!isKpi && hasSecondDimension && injectSeparators(dimension2, styling.useSeparatorColumns).map((entry, index) => {
if (entry.isSeparator) {
const separatorStyle = {
color: 'white',
fontFamily: styling.options.fontFamily,
fontSize: `${13 + styling.headerOptions.fontSizeAdjustment}px`
};
return (
<th
className="empty"
key={index}
style={separatorStyle}
>
*
</th>
);
}
return (
<ColumnHeader
baseCSS={baseCSS}
cellSuffix={general.cellSuffix}
colSpan={measurements.length}
entry={entry}
key={entry.displayValue}
qlik={qlik}
styling={styling}
/>
);
})}
</tr>
{!isKpi && hasSecondDimension && (
<tr>
{injectSeparators(dimension2, styling.useSeparatorColumns).map((dimensionEntry, index) => {
if (dimensionEntry.isSeparator) {
const separatorStyle = {
color: 'white',
fontFamily: styling.options.fontFamily,
fontSize: `${12 + styling.headerOptions.fontSizeAdjustment}px`
};
return (
<th
className="empty"
key={index}
style={separatorStyle}
>
*
</th>
);
}
return measurements.map(measurementEntry => (
<MeasurementColumnHeader
baseCSS={baseCSS}
dimensionEntry={dimensionEntry}
general={general}
hasSecondDimension={hasSecondDimension}
key={`${measurementEntry.displayValue}-${measurementEntry.name}-${dimensionEntry.name}`}
measurement={measurementEntry}
styling={styling}
/>
));
})}
</tr>
)}
</tbody>
</table>
</div>
);
};
HeadersTable.propTypes = {
data: PropTypes.shape({
headers: PropTypes.shape({
dimension1: PropTypes.array,
dimension2: PropTypes.array,
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,
styling: PropTypes.shape({
headerOptions: PropTypes.shape({}),
options: PropTypes.shape({})
}).isRequired,
isKpi: PropTypes.bool.isRequired
};
export default HeadersTable;

View File

@@ -0,0 +1,43 @@
import merge from 'lodash.merge';
import Model from './index.componentModel';
import Component from './index.jsx';
import { mountedComponent } from 'test-utilities';
import sampleState from 'test-utilities/capex-sample-state';
describe('<HeadersTable />', () => {
const { data, general, styling } = sampleState;
const defaultProps = {
data,
general,
qlik: {
backendApi: {
selectValues: () => {}
},
inEditState: () => {}
},
styling
};
function setup (otherProps = {}) {
const props = merge(defaultProps, otherProps);
return mountedComponent(Model, Component, props);
}
it('should render without exploding when 2 dimensions', () => {
const model = setup();
expect(model.component).toBeDefined();
});
it('should render without exploding when 1 dimension', () => {
const noSecondDimensionProps = {
data: {
...defaultProps.data.headers,
dimension2: []
}
};
const model = setup(noSecondDimensionProps);
expect(model.component).toBeDefined();
});
});

View File

@@ -0,0 +1,81 @@
import React from 'react';
import PropTypes from 'prop-types';
import { HEADER_FONT_SIZE } from '../initialize-transformed';
import Tooltip from '../tooltip/index.jsx';
const MeasurementColumnHeader = ({ baseCSS, general, hasSecondDimension, measurement, styling }) => {
const title = `${measurement.name} ${measurement.magnitudeLabelSuffix}`;
const { fontSizeAdjustment } = styling.headerOptions;
const isMediumFontSize = fontSizeAdjustment === HEADER_FONT_SIZE.MEDIUM;
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,
fontSize: `${baseFontSize + fontSizeAdjustment}px`,
height: isMediumFontSize ? '45px' : '35px',
verticalAlign: 'middle'
};
return (
<th
className={`${cellClass}${general.cellSuffix}`}
style={cellStyle}
>
<Tooltip
tooltipText={title}
styling={styling}
>
{title}
</Tooltip>
</th>
);
}
const style = {
...baseCSS,
fontSize: `${15 + fontSizeAdjustment}px`,
height: isMediumFontSize ? '90px' : '70px',
verticalAlign: 'middle'
};
return (
<th
className={`grid-cells2${general.cellSuffix}`}
style={style}
>
<Tooltip
tooltipText={title}
styling={styling}
>
{title}
</Tooltip>
</th>
);
};
MeasurementColumnHeader.defaultProps = {
hasSecondDimension: false
};
MeasurementColumnHeader.propTypes = {
baseCSS: PropTypes.shape({}).isRequired,
general: PropTypes.shape({
cellSuffix: PropTypes.string.isRequired
}).isRequired,
hasSecondDimension: PropTypes.bool,
measurement: PropTypes.shape({
name: PropTypes.string.isRequired
}).isRequired,
styling: PropTypes.shape({
headerOptions: PropTypes.shape({
fontSizeAdjustment: PropTypes.number.isRequired
}).isRequired
}).isRequired
};
export default MeasurementColumnHeader;

View File

@@ -1,37 +1,90 @@
import '@babel/polyfill';
import paint from './paint';
import definition from './definition';
import { initializeDataCube, initializeDesignList } from './dataset';
import initializeStore from './store';
import React from 'react';
import ReactDOM from 'react-dom';
import Root from './root.jsx';
import './main.less';
if (!window._babelPolyfill) { // eslint-disable-line no-underscore-dangle
require('@babel/polyfill'); // eslint-disable-line global-require
}
export default {
controller: [
'$scope',
'$timeout',
function () { }
function controller () {}
],
design: {
dimensions: {
max: 1,
min: 0
}
},
data: {
dimensions: {
max: function (nMeasures) {
return nMeasures < 9 ? 2 : 1;
},
min: 1,
uses: 'dimensions'
},
measures: {
max: function (nDims) {
return nDims < 2 ? 9 : 8;
},
min: 1,
uses: 'measures'
}
},
definition,
initialProperties: {
version: 1.0,
qHyperCubeDef: {
qDimensions: [],
qInitialDataFetch: [
{
qHeight: 1000,
qHeight: 1,
qWidth: 10
}
],
qMeasures: []
}
},
paint ($element, layout) {
try {
paint($element, layout, this);
} catch (exception) {
console.error(exception); // eslint-disable-line no-console
throw exception;
}
support: {
export: true,
exportData: true,
snapshot: true
},
paint: async function ($element, layout) {
const dataCube = await initializeDataCube(this, layout);
const designList = await initializeDesignList(this, layout);
const state = await initializeStore({
$element,
component: this,
dataCube,
designList,
layout
});
const editmodeClass = this.inAnalysisState() ? '' : 'edit-mode';
const jsx = (
<Root
editmodeClass={editmodeClass}
qlik={this}
state={state}
/>
);
ReactDOM.render(jsx, $element[0]);
},
snapshot: {
canTakeSnapshot: true
},
setSnapshotData: async function (snapshotLayout) {
snapshotLayout.snapshotData.dataCube = await initializeDataCube(this, snapshotLayout);
snapshotLayout.snapshotData.designList = await initializeDesignList(this, snapshotLayout);
return snapshotLayout;
},
version: 1.0
};

View File

@@ -0,0 +1,363 @@
import { distinctArray } from './utilities';
export const HEADER_FONT_SIZE = {
SMALL: -1,
MEDIUM: 1
};
function getAlignment (option) {
const alignmentOptions = {
1: 'left',
2: 'center',
3: 'right'
};
return alignmentOptions[option] || 'left';
}
function getFontSizeAdjustment (option) {
const fontSizeAdjustmentOptions = {
1: HEADER_FONT_SIZE.SMALL,
2: HEADER_FONT_SIZE.MEDIUM
};
return fontSizeAdjustmentOptions[option] || 0;
}
function getCellSuffix (option) {
const cellSuffixOptions = {
1: '-s',
3: '-l'
};
return cellSuffixOptions[option] || '';
}
function getMeasurementFormat (measurement) {
if (measurement.qNumFormat.qType === 'U' || measurement.qNumFormat.qFmt === '##############') {
return '#.##0';
} else if (measurement.qNumFormat.qType === 'R') {
return measurement.qNumFormat.qFmt.replace(/(|)/gi, '');
}
return measurement.qNumFormat.qFmt;
}
function getMagnitudeLabelSuffix (magnitudeOption) {
const magnitudeLabelSuffixOptions = {
'k': ' (k)',
'm': ' (m)'
};
return magnitudeLabelSuffixOptions[magnitudeOption] || '';
}
function generateMeasurements (information) {
return information.map(measurement => {
const format = getMeasurementFormat(measurement);
const formatMagnitude = format.substr(format.length - 1).toLowerCase();
const transformedMeasurement = {
format,
magnitudeLabelSuffix: getMagnitudeLabelSuffix(formatMagnitude),
name: measurement.qFallbackTitle
};
return transformedMeasurement;
});
}
function generateDimensionEntry (information, data) {
return {
displayValue: data.qText || data.qNum,
elementNumber: data.qElemNumber,
name: information.qFallbackTitle,
value: data.qNum
};
}
function generateMatrixCell ({ cell, dimension1Information, dimension2Information, measurementInformation }) {
const matrixCell = {
displayValue: cell.qText,
format: measurementInformation.format,
magnitude: measurementInformation.magnitudeLabelSuffix.substring(
measurementInformation.magnitudeLabelSuffix.length - 2,
measurementInformation.magnitudeLabelSuffix.length - 1
),
magnitudeLabelSuffix: measurementInformation.magnitudeLabelSuffix,
name: measurementInformation.name,
parents: {
dimension1: {
elementNumber: dimension1Information.qElemNumber,
header: dimension1Information.qText
},
measurement: {
header: measurementInformation.name,
index: measurementInformation.index
}
},
value: cell.qNum
};
if (dimension2Information) {
matrixCell.parents.dimension2 = {
elementNumber: dimension2Information.qElemNumber
};
}
return matrixCell;
}
let lastRow = 0;
function generateDataSet (
component, dimensionsInformation, measurementsInformation, dataCube) {
const measurements = generateMeasurements(measurementsInformation);
let dimension1 = [];
let dimension2 = [];
let matrix = [];
const hasSecondDimension = dimensionsInformation.length > 1;
dataCube.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;
}
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;
});
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);
}
}
}
matrix[appendToRowIndex] = matrixRow;
});
// filter header dimensions to only have distinct values
dimension1 = distinctArray(dimension1);
dimension2 = distinctArray(dimension2);
// Make sure all rows are saturated, otherwise data risks being displayed in the wrong column
matrix = matrix.map((row, rowIndex) => {
if ((hasSecondDimension && row.length == (dimension2.length * measurements.length))
|| (!hasSecondDimension && row.length == measurements.length)) {
// Row is saturated
return row;
}
// Row is not saturated, so must add empty cells to fill the gaps
let newRow = [];
if (hasSecondDimension) {
// Got a second dimension, so need to add measurements for all values of the second dimension
let rowDataIndex = 0;
dimension2.forEach(dim => {
rowDataIndex = appendMissingCells(
row, newRow, rowDataIndex, measurements, rowIndex, dim.elementNumber);
});
} else {
appendMissingCells(row, newRow, 0, measurements, rowIndex);
}
return newRow;
});
return {
dimension1: dimension1,
dimension2: dimension2,
matrix,
measurements
};
}
/*
* Appends the cells of the source row, as well as those missing, to the destination row, starting
* from the given source index. Returns the source index of the next source cell after this has
* completed. If there is a second dimension the dim2ElementNumber should be set to the current
* index of the dimension2 value being processed.
*/
function appendMissingCells (
sourceRow, destRow, sourceIndex, measurements, dim1ElementNumber, dim2ElementNumber = -1) {
let index = sourceIndex;
measurements.forEach((measurement, measureIndex) => {
if (index < sourceRow.length
&& (dim2ElementNumber === -1
|| sourceRow[index].parents.dimension2.elementNumber === dim2ElementNumber)
&& sourceRow[index].parents.measurement.header === measurement.name) {
// Source contains the expected cell
destRow.push(sourceRow[index]);
index++;
} else {
// Source doesn't contain the expected cell, so add empty
destRow.push({
displayValue: '',
parents: {
dimension1: { elementNumber: dim1ElementNumber },
dimension2: { elementNumber: dim2ElementNumber },
measurement: {
header: measurement.name,
index: measureIndex
}
}
});
}
});
return index;
}
function initializeTransformed ({ $element, component, dataCube, designList, layout }) {
const dimensionsInformation = component.backendApi.getDimensionInfos();
const measurementsInformation = component.backendApi.getMeasureInfos();
const dimensionCount = layout.qHyperCube.qDimensionInfo.length;
const rowCount = component.backendApi.getRowCount();
const maxLoops = layout.maxloops;
const {
dimension1,
dimension2,
measurements,
matrix
} = generateDataSet(component, dimensionsInformation, measurementsInformation, dataCube);
const customSchemaBasic = [];
const customSchemaFull = [];
let customHeadersCount = 0;
if (designList && designList.length > 0) {
const headers = designList[0].split(';');
customHeadersCount = headers.length;
for (let lineNumber = 0; lineNumber < designList.length; lineNumber += 1) {
customSchemaFull[lineNumber] = new Array(headers.length);
const data = designList[lineNumber].split(';');
if (data.length === headers.length) {
for (let headerIndex = 0; headerIndex < headers.length; headerIndex += 1) {
[customSchemaBasic[lineNumber]] = data;
customSchemaFull[lineNumber][headerIndex] = data[headerIndex];
}
}
}
}
// 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 = {
data: {
headers: {
dimension1, // column headers
dimension2, // parent row headers if exists
measurements // row headers, looped for each dimension2 if exists
},
matrix, // 2d array of all rows/cells to render in body of datatable
meta: {
dimensionCount: dimensionsInformation.length
}
},
general: {
allowExcelExport: layout.allowexportxls,
allowFilteringByClick: layout.filteroncellclick,
cellSuffix: getCellSuffix(layout.columnwidthslider), // TOOD: move to matrix cells or is it headers.measurements?
errorMessage: layout.errormessage,
footnote: layout.footnote,
maxLoops,
subtitle: layout.subtitle,
title: layout.title
},
selection: {
dimensionSelectionCounts: dimensionsInformation.map(dimensionInfo => dimensionInfo.qStateCounts.qSelected)
},
styling: {
customCSV: {
basic: customSchemaBasic,
count: customHeadersCount,
full: customSchemaFull
},
hasCustomFileStyle: Boolean(designList),
headerOptions: {
alignment: getAlignment(layout.HeaderAlign),
colorSchema: layout.HeaderColorSchema.color,
fontSizeAdjustment: getFontSizeAdjustment(layout.lettersizeheader),
textColor: layout.HeaderTextColorSchema.color
},
options: {
backgroundColor: layout.rowEvenBGColor,
backgroundColorOdd: layout.rowOddBGColor,
color: layout.BodyTextColorSchema,
fontFamily: layout.FontFamily,
fontSizeAdjustment: getFontSizeAdjustment(layout.lettersize),
textAlignment: layout.cellTextAlignment
},
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
},
colors: {
poor: {
color: layout.conditionalcoloring.color_poor,
textColor: layout.conditionalcoloring.textcolor_poor
},
fair: {
color: layout.conditionalcoloring.color_fair,
textColor: layout.conditionalcoloring.textcolor_fair
},
good: {
color: layout.conditionalcoloring.color_good,
textColor: layout.conditionalcoloring.textcolor_good
}
}
},
symbolForNulls: layout.symbolfornulls,
usePadding: layout.indentbool,
useSeparatorColumns: dimensionCount === 1 ? false : layout.separatorcols
}
};
if (rowCount > lastRow && rowCount <= (maxLoops * 1000)) {
const requestPage = [
{
qHeight: Math.min(1000, rowCount - lastRow),
qLeft: 0,
qTop: matrix.length,
qWidth: 10 // should be # of columns
}
];
component.backendApi.getData(requestPage).then(() => {
component.paint($element, layout);
});
}
return transformedProperties;
}
export default initializeTransformed;

View File

@@ -0,0 +1,2 @@
export { default as LinkedScrollWrapper } from './linked-scroll-wrapper.jsx';
export { default as LinkedScrollSection } from './linked-scroll-section.jsx';

View File

@@ -0,0 +1,29 @@
import React from 'react';
import PropTypes from 'prop-types';
import { LinkedScrollContext } from './linked-scroll-wrapper.jsx';
class LinkedScrollSection extends React.PureComponent {
static contextType = LinkedScrollContext;
componentDidMount () {
const { link } = this.context;
link(this);
}
componentWillUnmount () {
const { unlink } = this.context;
unlink(this);
}
render () {
const { children } = this.props;
return children;
}
}
LinkedScrollSection.propTypes = {
children: PropTypes.any
};
export default LinkedScrollSection;

View File

@@ -0,0 +1,82 @@
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
export const LinkedScrollContext = React.createContext();
class LinkedScrollWrapper extends React.PureComponent {
constructor (props) {
super(props);
this.linkComponent = this.linkComponent.bind(this);
this.unlinkComponent = this.unlinkComponent.bind(this);
this.handleScroll = this.handleScroll.bind(this);
this.scrollElements = [];
this.linkActions = {
link: this.linkComponent,
unlink: this.unlinkComponent
};
}
linkComponent (component) {
// eslint-disable-next-line react/no-find-dom-node
const node = ReactDOM.findDOMNode(component);
const element = {
component,
node
};
this.scrollElements.push(element);
node.onscroll = this.handleScroll.bind(this, element);
}
unlinkComponent (component) {
const componentIndex = this.scrollElements.map(element => element.component).indexOf(component);
if (componentIndex !== -1) {
this.scrollElements.removeAt(componentIndex);
// eslint-disable-next-line react/no-find-dom-node
const node = ReactDOM.findDOMNode(component);
node.onscroll = null;
}
}
handleScroll (element) {
window.requestAnimationFrame(() => {
this.sync(element);
});
}
sync (scrollElement) {
this.scrollElements.forEach(element => {
if (scrollElement === element) {
return;
}
element.node.onscroll = null;
if (element.component.props.linkHorizontal) {
element.node.scrollLeft = scrollElement.node.scrollLeft;
}
if (element.component.props.linkVertical) {
element.node.scrollTop = scrollElement.node.scrollTop;
}
window.requestAnimationFrame(() => {
element.node.onscroll = this.handleScroll.bind(this, element);
});
});
}
render () {
const { children } = this.props;
return (
<LinkedScrollContext.Provider value={this.linkActions}>
{children}
</LinkedScrollContext.Provider>
);
}
}
LinkedScrollWrapper.propTypes = {
children: PropTypes.any
};
export default LinkedScrollWrapper;

View File

@@ -1,215 +1,309 @@
/* eslint-disable */
.qv-object-qlik-smart-pivot {
@TableBorder: 1px solid #d3d3d3;
@KpiTableWidth: 230px;
._cell(@Width: 50px) {
min-width: @Width!important;
max-width: @Width!important;
cursor: pointer;
line-height: 1em!important;
*,
*::before,
*::after {
box-sizing: border-box;
}
div.qv-object-content-container {
overflow-x: scroll;
overflow-y: hidden;
z-index: 110;
}
.edit-mode {
pointer-events: none;
}
.icon-xls {
text-align: left;
}
._cell(@Width: 50px) {
min-width: @Width !important;
max-width: @Width !important;
cursor: pointer;
line-height: 1em !important;
}
button {
width: 100%;
}
div.qv-object-content-container {
z-index: 110;
}
table {
border-collapse: collapse;
border-spacing: 0;
width: auto;
border-left: @TableBorder;
border-right: @TableBorder;
border-top: @TableBorder;
}
.icon-xls {
text-align: left;
}
td, th {
border: 1px solid #ffffff;
padding: 5px;
border-collapse: collapse;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
cursor: default;
}
button {
width: 100%;
}
.empty {
width: 3%;
background: #ffffff;
min-width: 4px !important;
max-width: 4px !important;
}
table {
border-collapse: separate;
border-spacing: 1px;
width: auto;
}
th.main-kpi {
text-align: center;
vertical-align: middle;
border-bottom: @TableBorder;
}
td,
th {
padding: 5px !important; // prevent overwriting from single object
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
cursor: default;
.numeric {
text-align: right;
}
/*This is for wrap text in headers*/
.wrapclass25 {
width: 100%;
height: 25px;
white-space: pre-line;
overflow: hidden;
display: block;
}
> div {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-family: inherit;
}
}
.wrapclass45 {
width: 100%;
height: 45px;
white-space: pre-line;
overflow: hidden;
display: block;
}
.empty {
width: 3%;
background: #fff;
min-width: 4px !important;
max-width: 4px !important;
}
.wrapclass70 {
width: 100%;
height: 70px;
white-space: pre-line;
overflow: hidden;
display: inline-block;
vertical-align: middle;
line-height: 20px;
}
th.main-kpi {
text-align: center;
vertical-align: middle;
}
.wrapclassEmpty {
width: 100%;
}
/*******************/
/* Medium column size*/
/*******************/
.grid-cells { ._cell(70px); }
.grid-cells2 { ._cell(70px); }
.grid-cells-small { ._cell(52px); }
.grid-cells2-small { ._cell(52px); }
/*******************/
/* Small column size*/
/*******************/
.grid-cells-s { ._cell(67px); }
.grid-cells2-s { ._cell(67px); }
.grid-cells-small-s { ._cell(52px); }
.grid-cells2-small-s { ._cell(52px); }
/*******************/
/* large column size*/
/*******************/
.grid-cells-l { ._cell(82px); }
.grid-cells2-l { ._cell(82px); }
.grid-cells-small-l { ._cell(66px); }
.grid-cells2-small-l { ._cell(66px); }
.numeric {
text-align: right;
}
/*END OF GRID CELLS*/
/*First Column*/
.fdim-cells {
min-width: 230px !Important;
max-width: 230px !Important;
cursor: pointer;
background-color: white;
}
// *****************
// Medium column size
// *****************
.fdim-cells:hover {
/*cursor: default;*/
background-color: #808080 !important;
color: #ffffff;
}
.grid-cells {
position: relative;
._cell(70px);
}
tbody tr:hover {
cursor: default;
background-color: #808080 !important;
color: #ffffff;
}
.grid-cells2 {
._cell(70px);
}
.grid-cells-header {
padding: 0px;
}
.grid-cells-small {
._cell(52px);
}
.grid-cells-title {
min-width: 522px;
}
.grid-cells2-small {
._cell(52px);
}
.grid-cells:before {
content: "\00a0";
}
// *****************
// Small column size
// *****************
.grid-cells-s {
._cell(67px);
}
.grid-cells2-s {
._cell(67px);
}
.grid-cells-small-s {
._cell(52px);
}
.grid-cells2-small-s {
._cell(52px);
}
// *****************
// Large column size
// *****************
.grid-cells-l {
._cell(82px);
}
.grid-cells2-l {
._cell(82px);
}
.grid-cells-small-l {
._cell(66px);
}
.grid-cells2-small-l {
._cell(66px);
}
// END OF GRID CELLS
// First Column
.fdim-cells {
min-width: 230px !Important;
max-width: 230px !Important;
cursor: pointer;
background-color: #fff;
}
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;
height: 50px;
width: 350px;
}
.header-wrapper {
position: absolute;
top: 0;
z-index: 1;
}
/*popups for headers*/
.tooltip {
position: fixed !important;
color: RGB(70,70,70);
background-color: RGB(245,239,207);
text-align: center;
border: groove;
position: fixed !important;
color: rgb(70, 70, 70);
background-color: rgb(245, 239, 207);
text-align: center;
border: groove;
}
/*end popups*/
.row-wrapper {
position: absolute;
top: 97px;
height: calc(~"100% - 97px");
overflow-x: hidden;
overflow-y: scroll;
padding: 0;
margin-top: 0;
.root {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
height: 100%;
width: 100%;
}
.kpi-table .fdim-cells, .data-table td {
line-height: 1em!important;
}
.data-table .fdim-cells {
display: none;
.kpi-table .fdim-cells,
.data-table td {
line-height: 1em !important;
}
.kpi-table {
width: @KpiTableWidth !important;
overflow: hidden !important;
display: table;
height: 100%;
margin: 0;
padding: 0;
z-index: 100;
position: absolute;
top: 0;
left: 0;
border-right: 1px solid white;
box-shadow: 4px 2px 8px #e1e1e1;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
flex: none;
width: @KpiTableWidth !important;
margin: 0;
padding: 0;
.header-wrapper {
flex: none;
box-shadow: 4px 2px 8px #e1e1e1;
}
.row-wrapper {
overflow: scroll;
margin: 0;
margin-bottom: 8px;
padding: 0;
box-shadow: 4px 2px 8px #e1e1e1;
min-height: 0; /* This is to make flex size-filling work */
/* Adapt for Edge */
@supports (-ms-ime-align: auto) {
margin-bottom: 16px;
}
/* Adapt for IE11 */
@media screen and (-ms-high-contrast: none) {
margin-bottom: 16px;
}
}
}
.kpi-table .row-wrapper {
overflow: hidden;
.row-wrapper .fdim-cells {
padding-left: 12px;
}
.data-table {
width: 272px !important;
float: left;
display: table;
height: 100%;
z-index: 90;
position: absolute;
margin-left: @KpiTableWidth + 13px;
-ms-overflow-style: none;
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;
margin-right: 8px;
}
.row-wrapper {
overflow: scroll;
margin: 0;
padding: 0;
min-height: 0; /* This is to make flex size-filling work */
/* Style scrollbar for FF */
scrollbar-width: thin;
scrollbar-color: #d3d3d3 transparent;
}
/* Adapt for Edge */
@supports (-ms-ime-align: auto) {
.header-wrapper {
margin-right: 16px;
}
}
/* Adapt for IE11 */
@media screen and (-ms-high-contrast: none) {
width: 100%;
height: 100%;
.header-wrapper {
margin-right: 16px;
}
}
}
// hide scrollbars
.kpi-table .header-wrapper,
.kpi-table .row-wrapper,
.data-table .header-wrapper {
// stylelint-disable-next-line property-no-unknown
scrollbar-width: none;
-ms-overflow-style: none; // IE 10+
-moz-overflow: -moz-scrollbars-none; // Firefox
&::-webkit-scrollbar {
display: none; // Safari and Chrome
}
}
.tooltip-wrapper {
min-width: 25px;
position: fixed;
padding: 5px;
padding-top: 8px;
background-color: #404040;
z-index: 100;
pointer-events: none;
border-radius: 5px;
height: 30px;
width: auto;
opacity: 0.9;
text-align: center;
transform: translate(-50%, -110%);
&::after {
content: "";
position: absolute;
bottom: -10px;
left: 50%;
border-width: 10px 10px 0;
border-style: solid;
border-color: #404040 transparent;
margin-left: -10px;
pointer-events: none;
}
> p {
color: #fff;
}
}
}
/* eslint-enable */

View File

@@ -13,7 +13,7 @@ export function ApplyPreMask (mask, value) { // aqui
case '+#,##0':
return (addSeparators(value, ',', '.', 0));
default:
return (ApplyMask(mask.substring(0, mask.indexOf(';')), value));
return (applyMask(mask.substring(0, mask.indexOf(';')), value));
}
} else {
const vMyValue = value * -1;
@@ -30,46 +30,47 @@ export function ApplyPreMask (mask, value) { // aqui
case '-#,##0':
return (`(${addSeparators(vMyValue, ',', '.', 0)})`);
default:
return (`(${ApplyMask(vMyMask, vMyValue)})`);
return (`(${applyMask(vMyMask, vMyValue)})`);
}
}
} else {
return (ApplyMask(mask, value));
return (applyMask(mask, value));
}
}
function ApplyMask (mask, value) {
if (!mask || isNaN(Number(value))) {
return value; // return as it is.
function applyMask (originalMask, originalValue) {
if (!originalMask || isNaN(Number(originalValue))) {
return originalValue;
}
let isNegative, result, decimal, group, posLeadZero, posTrailZero, posSeparator,
part, szSep, integer,
// find prefix/suffix
len = mask.length,
start = mask.search(/[0-9\-\+#]/),
prefix = start > 0 ? mask.substring(0, start) : '',
// reverse string: not an ideal method if there are surrogate pairs
str = mask.split('').reverse()
.join(''),
end = str.search(/[0-9\-\+#]/),
offset = len - end,
substr = mask.substring(offset, offset + 1),
indx = offset + ((substr === '.' || (substr === ',')) ? 1 : 0),
suffix = end > 0 ? mask.substring(indx, len) : '';
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
mask = mask.substring(start, indx);
let mask = originalMask.substring(start, index);
// convert any string to number according to formation sign.
value = mask.charAt(0) === '-' ? -value : Number(value);
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);
decimal = (result && result[result.length - 1]) || '.'; // treat the right most symbol as decimal
group = (result && result[1] && result[0]) || ','; // treat the left most symbol as group separator
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);
@@ -78,16 +79,16 @@ function ApplyMask (mask, value) {
value = String(Number(value)); // convert number to string to trim off *all* trailing decimal zero(es)
// fill back any trailing zero according to format
posTrailZero = mask[1] && mask[1].lastIndexOf('0'); // look for last zero in format
part = value.split('.');
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);
}
szSep = mask[0].split(group); // look for separator
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.
posLeadZero = mask[0] && mask[0].indexOf('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]}`;
@@ -101,17 +102,17 @@ function ApplyMask (mask, value) {
// process the first group separator from decimal (.) only, the rest ignore.
// get the length of the last slice of split result.
posSeparator = (szSep[1] && szSep[szSep.length - 1].length);
const posSeparator = (szSep[1] && szSep[szSep.length - 1].length);
if (posSeparator) {
integer = value[0];
str = '';
offset = integer.length % posSeparator;
len = integer.length;
for (indx = 0; indx < len; indx++) {
str += integer.charAt(indx); // ie6 only support charAt for sz.
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 (!((indx - offset + 1) % posSeparator) && indx < len - posSeparator) {
if (!((index - offset + 1) % posSeparator) && index < len - posSeparator) {
str += group;
}
}

View File

@@ -1,210 +0,0 @@
import $ from 'jquery';
import { enableExcelExport } from './excel-export';
import HeaderWrapper from './header-wrapper.jsx';
import RowWrapper, { prepareProps } from './row-wrapper.jsx';
import initializeStore from './store';
import React from 'react';
// import ReactDOM from 'react-dom';
import { renderToStaticMarkup } from 'react-dom/server';
export default async function paint ($element, layout, component) {
const state = initializeStore({
$element,
layout,
component
});
const {
ArrayGetSelectedCount,
vNumDims,
ConceptMatrixColElem,
ConceptMatrixColElemTable,
ConceptMatrixRowElem,
vSeparatorCols
} = state.properties;
const rowWrapperProps = await prepareProps({
state: {
layout,
colors: state.colors,
...state.properties
}
});
const jsx = (
<React.Fragment>
<div className="kpi-table">
<HeaderWrapper
dimensionInfos={state.dimensionInfos}
measureInfos={state.measureInfos}
{...state.properties}
/>
<RowWrapper
colors={state.colors}
layout={layout}
{...state.properties}
{...rowWrapperProps}
/>
</div>
<div className="data-table">
<HeaderWrapper
dimensionInfos={state.dimensionInfos}
measureInfos={state.measureInfos}
{...state.properties}
/>
<RowWrapper
colors={state.colors}
layout={layout}
{...state.properties}
{...rowWrapperProps}
/>
</div>
</React.Fragment>
);
// TODO: switch to render when jquery interaction stuff in renderData is gone
// ReactDOM.render(jsx, $element[0]);
const html = renderToStaticMarkup(jsx);
$element.html(html);
RenderData();
// TODO: move jquery interactions into their respective components
// Hook up interactions and some html mangling
function RenderData () {
$('.data-table .row-wrapper').on('scroll', function () {
$(`[tid="${layout.qInfo.qId}"] .kpi-table .row-wrapper`).scrollTop($(this).scrollTop());
});
// on hover popup with cell value, only in headers
$('.header-wrapper th').hover(function () {
$('.tooltip').delay(500)
.show(0);
$('.header-wrapper th').children('.tooltip')
.remove();
const element = $(this);
const offset = element.offset();
const toolTip = $("<div class='tooltip'></div>");
toolTip.css({
top: offset.top,
left: offset.left
});
toolTip.text(element.text());
$('.header-wrapper th').append(toolTip);
}, () => {
$('.tooltip').delay(0)
.hide(0);
});
// allow making selections inside the table
$('.data-table td').on('click', function () {
if (layout.filteroncellclick == false) {
return;
}
const indextr = $(this).parent()
.parent()
.children()
.index($(this).parent()); // identifica la row
const indextd = $(this).parent()
.children()
.index($(this)); // identifica la col
let SelectRow = 0;
let SelectCol = 0;
SelectRow = ConceptMatrixRowElem[(indextr)];
// este if verifica primero si hay selecciones hechas en la dimensión, si las hay
// las reselecciona para poder borrar antes de poder seleccionar lo que quiero
// no es viable pedirle que seleccione a la vez elementos de 2 selecciones, se queda
// colgado el menú de confirm, por eso uso este sistema, que sí funciona.
// it can cause issues like error messages and wrong selections if there are null values
// and the check allow null values is active
if (vNumDims > 1 && indextd > 0) {
if (ArrayGetSelectedCount[1] > 0) {
const SelectB = JSON.parse(JSON.stringify(ConceptMatrixColElemTable));
component.backendApi.selectValues(1, SelectB, true);
$(this).toggleClass('selected');
}
SelectCol = ConceptMatrixColElemTable[(indextd)];
component.backendApi.selectValues(1, [SelectCol], true);
$(this).toggleClass('selected');
}
if (indextd > 0 && ArrayGetSelectedCount[0] > 0) {
const SelectA = JSON.parse(JSON.stringify(ConceptMatrixRowElem));
component.backendApi.selectValues(0, SelectA, true);
$(this).toggleClass('selected');
}
if (indextd > 0) {
component.backendApi.selectValues(0, [SelectRow], true);
$(this).toggleClass('selected');
}
});
// allow selections through the header of the second dimension
$('.header-wrapper th').on('click', function () {
const indextd = $(this).parent()
.children()
.index($(this)); // identifica la col
let SelectCol = 0;
if (vNumDims > 1 && indextd > 0) {
if (ArrayGetSelectedCount[1] > 0) {
const SelectB = JSON.parse(JSON.stringify(ConceptMatrixColElem));
component.backendApi.selectValues(1, SelectB, true);
$(this).toggleClass('selected');
}
if (vSeparatorCols) {
SelectCol = ConceptMatrixColElem[(Math.round(indextd / 2) - 1)];
} else {
SelectCol = ConceptMatrixColElem[(Math.round(indextd) - 1)];
}
component.backendApi.selectValues(1, [SelectCol], true);
$(this).toggleClass('selected');
}
});
// allow selections in desc dimension cells
$('.kpi-table td').on('click', function () {
const indextr = $(this).parent()
.parent()
.children()
.index($(this).parent()); // identifica la row
let SelectRow = 0;
SelectRow = ConceptMatrixRowElem[(indextr)];
if (ArrayGetSelectedCount[0] > 0) {
const SelectA = JSON.parse(JSON.stringify(ConceptMatrixRowElem));
component.backendApi.selectValues(0, SelectA, true);
$(this).toggleClass('selected');
}
component.backendApi.selectValues(0, [SelectRow], true);
$(this).toggleClass('selected');
});
enableExcelExport(layout, html);
// freeze first column
$('.qv-object-content-container').on('scroll', (t) => {
$('.kpi-table').css('left', `${Math.round(t.target.scrollLeft)}px`);
});
$('.kpi-table .row-wrapper tr').each(function () {
$(this).find('th:not(.fdim-cells)')
.remove();
$(this).find('td:not(.fdim-cells)')
.remove();
});
$('.kpi-table .header-wrapper tr').each(function () {
$(this).find('th:not(.fdim-cells)')
.remove();
});
}
}

View File

@@ -0,0 +1,5 @@
function Model (component) {
this.component = component;
}
export default Model;

64
src/root.jsx Normal file
View File

@@ -0,0 +1,64 @@
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 }) => (
<div className="root">
<LinkedScrollWrapper>
<div className={`kpi-table ${editmodeClass}`}>
<HeadersTable
data={state.data}
general={state.general}
isKpi
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}
isKpi={false}
qlik={qlik}
styling={state.styling}
/>
</LinkedScrollSection>
<LinkedScrollSection
linkHorizontal
linkVertical
>
<DataTable
data={state.data}
general={state.general}
qlik={qlik}
styling={state.styling}
/>
</LinkedScrollSection>
</div>
</LinkedScrollWrapper>
</div>
);
Root.propTypes = {
qlik: PropTypes.shape({}).isRequired,
state: PropTypes.shape({
data: PropTypes.object.isRequired,
general: PropTypes.object.isRequired,
styling: PropTypes.object.isRequired
}).isRequired,
editmodeClass: PropTypes.string.isRequired
};
export default Root;

29
src/root.spec.js Normal file
View File

@@ -0,0 +1,29 @@
import merge from 'lodash.merge';
import Model from './root.componentModel';
import Component from './root.jsx';
import { mountedComponent } from 'test-utilities';
import sampleState from 'test-utilities/capex-sample-state';
describe('<Root />', () => {
const state = sampleState;
const defaultProps = {
qlik: {
backendApi: {
selectValues: () => {}
},
inEditState: () => {}
},
state
};
function setup (otherProps = {}) {
const props = merge(defaultProps, otherProps);
return mountedComponent(Model, Component, props);
}
it('should render without exploding', () => {
const model = setup();
expect(model.component).toBeDefined();
});
});

View File

@@ -1,81 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import StyleBuilder from './style-builder';
class RowList extends React.PureComponent {
generatePaddingTextElement (hasCustomFileStyle) {
const { vPadding, vFontFamily } = this.props;
if (vPadding && !hasCustomFileStyle) {
const paddingStyle = {
fontFamily: vFontFamily,
marginLeft: '15px'
};
return (
<span style={paddingStyle} />
);
}
return null;
}
render () {
const {
vLetterSize,
vCustomFileBool,
vFontFamily,
tableData,
MeasurementsComponent
} = this.props;
return (
<tbody>
{tableData.map((row, rowNumber) => {
const rowHeaderText = row[0] || '';
if (rowHeaderText === '-') {
return null;
}
const styleBuilder = new StyleBuilder(this.props);
if (vCustomFileBool) {
styleBuilder.parseCustomFileStyle(rowHeaderText);
} else {
styleBuilder.applyStandardAttributes(rowNumber);
styleBuilder.applyCustomStyle({ fontSize: (14 + vLetterSize) + 'px' });
}
const rowStyle = {
fontFamily: vFontFamily,
width: '230px',
...styleBuilder.getStyle()
};
const paddingTextElement = this.generatePaddingTextElement(styleBuilder.hasCustomFileStyle());
const measurementsProps = {
rowHeaderText,
rowNumber,
styleBuilder
};
return (
<tr key={rowNumber}>
<td
className="fdim-cells"
style={rowStyle}
>
{paddingTextElement}
{rowHeaderText}
</td>
<MeasurementsComponent
columnText={rowHeaderText}
{...this.props}
{...measurementsProps}
/>
</tr>
);
})}
</tbody>
);
}
}
RowList.propTypes = {
tableData: PropTypes.array.isRequired
};
export default RowList;

View File

@@ -1,129 +0,0 @@
import $ from 'jquery';
import PropTypes from 'prop-types';
import React from 'react';
import ElseDimensionMeasures from './else-dimension-measures.jsx';
import RowList from './row-list.jsx';
import SingleDimensionMeasures from './single-dimension-measures.jsx';
const RowWrapper = props => {
const {
ConceptMatrix,
ConceptMatrixPivot,
vNumDims
} = props;
let MeasurementsComponent,
tableData;
if (vNumDims === 1) {
tableData = ConceptMatrix;
MeasurementsComponent = SingleDimensionMeasures;
} else {
tableData = ConceptMatrixPivot.filter(array => array.length);
MeasurementsComponent = ElseDimensionMeasures;
}
return (
<div className="row-wrapper">
<table>
<RowList
MeasurementsComponent={MeasurementsComponent}
tableData={tableData}
{...props}
/>
</table>
</div>
);
};
RowWrapper.propTypes = {
ConceptMatrix: PropTypes.array.isRequired,
ConceptMatrixPivot: PropTypes.array.isRequired
};
export default RowWrapper;
export async function prepareProps ({ state }) {
const { colors, layout, vAllSemaphores, vDynamicColorBody, vDynamicColorBodyP } = state;
const props = {
colors,
vCustomFileBool: layout.customfilebool,
vCustomFile: layout.customfile,
vPadding: layout.indentbool,
vPaddingText: '',
vGlobalComas: 0,
vGlobalComas2: 0,
vGlobalComment: 0,
vGlobalCommentColor: '',
vGlobalFontSize: 0,
vComas: 0,
vMedium: false,
vFontSize: '',
vColorText: layout.BodyTextColorSchema,
vDivide: 1,
vSymbolForNulls: layout.symbolfornulls,
vDynamicColorBody: 'vColLib' + layout.ColorSchema,
vDynamicColorBodyP: 'vColLib' + layout.ColorSchema + 'P',
vAllMetrics: layout.allmetrics,
MetricsAffectedMatrix: JSON.parse('[' + layout.metricssemaphore + ']'),
vColorMetric1: layout.colorstatus1.color,
vColorMetric2: layout.colorstatus2.color,
vColorMetric3: layout.colorstatus3.color,
vColorMetric1Text: layout.colorstatus1text.color,
vColorMetric2Text: layout.colorstatus2text.color,
vColorMetric3Text: layout.colorstatus3text.color,
vColorSemaphore: '',
vColorSemaphoreText: '',
vCritic: layout.metricsstatus1,
vMMedium: layout.metricsstatus2,
CustomArray: new Array(),
CustomArrayBasic: new Array(),
vNumCustomHeaders: 0,
vColumnText: '',
vColumnNum: '',
vMaskNum: 0,
StyleTags: '',
vColorSchema: colors[vDynamicColorBody],
vColorSchemaP: colors[vDynamicColorBodyP],
vAllSemaphores: layout.allsemaphores,
ConceptsAffectedMatrix: new Array(10)
};
if (vAllSemaphores == false) {
props.ConceptsAffectedMatrix[0] = layout.conceptsemaphore1;
props.ConceptsAffectedMatrix[1] = layout.conceptsemaphore2;
props.ConceptsAffectedMatrix[2] = layout.conceptsemaphore3;
props.ConceptsAffectedMatrix[3] = layout.conceptsemaphore4;
props.ConceptsAffectedMatrix[4] = layout.conceptsemaphore5;
props.ConceptsAffectedMatrix[5] = layout.conceptsemaphore6;
props.ConceptsAffectedMatrix[6] = layout.conceptsemaphore7;
props.ConceptsAffectedMatrix[7] = layout.conceptsemaphore8;
props.ConceptsAffectedMatrix[8] = layout.conceptsemaphore9;
props.ConceptsAffectedMatrix[9] = layout.conceptsemaphore10;
}
function ReadCustomSchema () {
var Url = '/Extensions/qlik-smart-pivot/' + props.vCustomFile;
return $.get(Url).then(function (response) {
var allTextLines = response.split(/\r\n|\n/);
var headers = allTextLines[0].split(';');
props.vNumCustomHeaders = headers.length;
for (var i = 0; i < allTextLines.length; i++) {
props.CustomArray[i] = new Array(headers.length);
var data = allTextLines[i].split(';');
if (data.length == headers.length) {
for (var j = 0; j < headers.length; j++) {
props.CustomArrayBasic[i] = data[0];
props.CustomArray[i][j] = data[j];
}
}
}
});
}
const hasCustomSchema = (props.vCustomFileBool && props.vCustomFile.length > 4);
const schemaPromise = hasCustomSchema ? ReadCustomSchema() : Promise.resolve();
await schemaPromise;
return props;
}

View File

@@ -1,159 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ApplyPreMask } from './masking';
import { addSeparators } from './utilities';
class SingleDimensionMeasures extends React.PureComponent {
render () {
const {
vFontFamily,
vSymbolForNulls,
vColorMetric1,
vColorMetric1Text,
vColorMetric2,
vColorMetric2Text,
vColorMetric3,
vColorMetric3Text,
ConceptMatrix,
vAllSemaphores,
ConceptsAffectedMatrix,
vAllMetrics,
MetricsAffectedMatrix,
vCritic,
vMMedium,
vNumMeasures,
MeasuresFormat,
rowNumber,
columnText,
styleBuilder
} = this.props;
// modified in here
let vColumnNum,
vMaskNum,
vColorSemaphore,
vColorSemaphoreText,
vDivide;
const measurementCells = [];
// TODO: map ConceptMatrix[rowNumber] into cells
for (var nMeasures2 = 1; nMeasures2 <= vNumMeasures; nMeasures2++) {
var vSpecialF = MeasuresFormat[nMeasures2 - 1].replace(/k|K|m|M/gi, '');
if (columnText.substring(0, 1) == '%') {
vColumnNum = ApplyPreMask('0,00%', ConceptMatrix[rowNumber][nMeasures2]);
vSpecialF = '0,00%';
} else {
const magnitude = MeasuresFormat[nMeasures2 - 1].substr(MeasuresFormat[nMeasures2 - 1].length - 1);
switch (magnitude.toLowerCase()) {
case 'k':
vDivide = 1000;
break;
case 'm':
vDivide = 1000000;
break;
default:
vDivide = 1;
break;
}
if (!isNaN(ConceptMatrix[rowNumber][nMeasures2])) {
vMaskNum = ConceptMatrix[rowNumber][nMeasures2];
if (vSpecialF.substring(vSpecialF.length - 1) == '%') {
vMaskNum = vMaskNum * 100;
}
switch (vSpecialF) {
case '#.##0':
vColumnNum = addSeparators((vMaskNum / vDivide), '.', ',', 0);
break;
case '#,##0':
vColumnNum = addSeparators((vMaskNum / vDivide), ',', '.', 0);
break;
default:
vColumnNum = ApplyPreMask(vSpecialF, (vMaskNum / vDivide));
break;
}
} else {
vColumnNum = vSymbolForNulls;
}
}
if (styleBuilder.hasComments()) {
vColumnNum = '.';
}
// apply the semaphore styles where needed
let cellStyle;
if ((vAllSemaphores || ConceptsAffectedMatrix.indexOf(columnText) >= 0) && (vAllMetrics || MetricsAffectedMatrix.indexOf(nMeasures2) >= 0) && !isNaN(ConceptMatrix[rowNumber][nMeasures2]) && !styleBuilder.hasComments()) {
if (ConceptMatrix[rowNumber][nMeasures2] < vCritic) {
vColorSemaphore = vColorMetric1;
vColorSemaphoreText = vColorMetric1Text;
} else {
if (ConceptMatrix[rowNumber][nMeasures2] < vMMedium) {
vColorSemaphore = vColorMetric2;
vColorSemaphoreText = vColorMetric2Text;
} else {
vColorSemaphore = vColorMetric3;
vColorSemaphoreText = vColorMetric3Text;
}
}
cellStyle = {
fontFamily: vFontFamily,
fontSize: styleBuilder.getStyle().fontSize,
color: vColorSemaphoreText,
backgroundColor: vColorSemaphore,
textAlign: 'right',
paddingLeft: '4px'
};
} else {
cellStyle = {
fontFamily: vFontFamily,
textAlign: 'right',
paddingLeft: '4px',
...styleBuilder.getStyle()
};
}
const measurementCell = (
<td key={nMeasures2} className="grid-cells' + sufixCells + '" style={cellStyle}>
{vColumnNum}
</td>
);
measurementCells.push(measurementCell);
}
return (
<React.Fragment>
{measurementCells}
</React.Fragment>
);
}
}
SingleDimensionMeasures.propTypes = {
vFontFamily: PropTypes.any,
vSymbolForNulls: PropTypes.any,
vColorMetric1: PropTypes.any,
vColorMetric1Text: PropTypes.any,
vColorMetric2: PropTypes.any,
vColorMetric2Text: PropTypes.any,
vColorMetric3: PropTypes.any,
vColorMetric3Text: PropTypes.any,
ConceptMatrix: PropTypes.any,
vAllSemaphores: PropTypes.any,
ConceptsAffectedMatrix: PropTypes.any,
vAllMetrics: PropTypes.any,
MetricsAffectedMatrix: PropTypes.any,
vCritic: PropTypes.any,
vMMedium: PropTypes.any,
vNumMeasures: PropTypes.any,
MeasuresFormat: PropTypes.any,
rowNumber: PropTypes.any,
columnText: PropTypes.any,
styleBuilder: PropTypes.any
};
export default SingleDimensionMeasures;

View File

@@ -1,299 +1,15 @@
import { onlyUnique } from './utilities';
import initializeTransformed from './initialize-transformed';
function initialize ({ $element, layout, component }) {
const colors = {
vColLibClean: layout.collibclean,
vColLibSoft: layout.collibsoft,
vColLibDark: layout.collibdark,
vColLibNight: layout.collibnight,
vColLibRed: layout.collibred,
vColLibOrange: layout.colliborange,
vColLibBlue: layout.collibblue,
vColLibGreen: layout.collibgreen,
vColLibViolete: layout.collibviolete,
vColLibCustom: layout.collibcustom,
vColLibCleanP: layout.collibcleanp,
vColLibSoftP: layout.collibsoftp,
vColLibDarkP: layout.collibdarkp,
vColLibNightP: layout.collibnightp,
vColLibRedP: layout.collibredp,
vColLibOrangeP: layout.colliborangep,
vColLibBlueP: layout.collibbluep,
vColLibGreenP: layout.collibgreenp,
vColLibVioleteP: layout.collibvioletep,
vColLibCustomP: layout.collibcustomp
};
const nMeasAux = 0;
const vMaxLoops = layout.maxloops;
const vErrorMessage = layout.errormessage;
const vDynamicColorHeader = `vColLib${layout.HeaderColorSchema}`;
const vHeaderColorSchema = colors[vDynamicColorHeader];
const vExportToExcel = layout.allowexportxls;
const vHeaderColorText = layout.HeaderTextColorSchema;
const vHeaderAlign = layout.HeaderAlign;
let vHeaderAlignText = '';
switch (vHeaderAlign) {
case 1:
vHeaderAlignText = 'left';
break;
case 2:
vHeaderAlignText = 'center';
break;
case 3:
vHeaderAlignText = 'right';
break;
}
let vLetterSizeHeader = 0;
switch (layout.lettersizeheader) {
case 1:
vLetterSizeHeader = -2;
break;
case 2:
vLetterSizeHeader = 0;
break;
case 3:
vLetterSizeHeader = 2;
break;
}
let vDimName = '';
const ConceptMatrixFirst = new Array();
const ConceptMatrixSecond = new Array();
let SecondHeaderLength = 0;
const LabelsArray = new Array();
const ExtraLabelsArray = new Array();
let vExtraLabel = '';
const vExcelButtonCode = '';
const ArrayGetSelectedCount = new Array();
let vNumDims = 0;
let vNumMeasures = 0;
let vNumMeasures2 = 0;
const MeasuresFormat = new Array();
let sufixCells = '';
switch (layout.columnwidthslider) {
case 1:
sufixCells += '-s';
break;
case 2:
sufixCells = String(sufixCells);
break;
case 3:
sufixCells += '-l';
break;
default:
sufixCells = String(sufixCells);
break;
}
const dim_count = layout.qHyperCube.qDimensionInfo.length;
const measure_count = layout.qHyperCube.qMeasureInfo.length;
let vSeparatorCols = layout.separatorcols;
if (dim_count == 1) {
vSeparatorCols = false;
}
const vFontFamily = layout.FontFamily;
let lastrow = 0;
const ConceptMatrix = new Array();
let ConceptMatrixRowElem = new Array();
let ConceptMatrixColElem = new Array();
const ConceptMatrixColElemTable = new Array();
const ConceptMatrixPivot = new Array();
let ConceptMatrixFirstClean = new Array();
let vLetterSize = 0;
switch (layout.lettersize) {
case 1:
vLetterSize = -2;
break;
case 2:
vLetterSize = -1;
break;
case 3:
vLetterSize = 2;
break;
}
const nRows = component.backendApi.getRowCount();
const dimensionInfos = component.backendApi.getDimensionInfos();
LabelsArray.push(dimensionInfos[0].qFallbackTitle);
ArrayGetSelectedCount.concat(dimensionInfos.map(dimensionInfo => dimensionInfo.qStateCounts.qSelected));
vNumDims += dimensionInfos.length;
const measureInfos = component.backendApi.getMeasureInfos();
measureInfos.forEach(measureInfo => {
vDimName = measureInfo.qFallbackTitle;
LabelsArray.push(vDimName);
let mfor = '';
if (measureInfo.qNumFormat.qType == 'U' || measureInfo.qNumFormat.qFmt == '##############') {
mfor = '#.##0'; // in case of undefined
} else if (measureInfo.qNumFormat.qType == 'R') {
mfor = measureInfo.qNumFormat.qFmt;
mfor = mfor.replace(/(|)/gi, '');
} else {
mfor = measureInfo.qNumFormat.qFmt;
}
MeasuresFormat.push(mfor);
switch (mfor.substr(mfor.length - 1).toLowerCase()) {
case 'm':
vExtraLabel = ' (M)';
ExtraLabelsArray.push(' (M)');
break;
case 'k':
vExtraLabel = ' (k)';
ExtraLabelsArray.push(' (k)');
break;
default:
vExtraLabel = '';
ExtraLabelsArray.push('');
break;
}
vNumMeasures++;
async function initialize ({ $element, layout, component, dataCube, designList }) {
const transformedProperties = await initializeTransformed({
$element,
component,
dataCube,
designList,
layout
});
component.backendApi.eachDataRow((t, a) => {
lastrow = t;
const vNumMeasuresPlus = vNumMeasures + 1;
ConceptMatrix[t] = new Array();
ConceptMatrix[t][0] = a[0].qText;
ConceptMatrixFirst[t] = a[0].qText;
ConceptMatrixRowElem[t] = a[0].qElemNumber;
let nMeasures = 0;
if (vNumDims == 1) {
for (nMeasures = 1; nMeasures <= vNumMeasures; nMeasures++) {
ConceptMatrix[t][nMeasures] = a[nMeasures].qNum;
}
} else {
ConceptMatrix[t][1] = a[1].qText;
ConceptMatrixColElem[t] = a[1].qElemNumber;
ConceptMatrixSecond[t] = a[1].qText;
// set the hipercube in a plain array without pivoting
for (nMeasures = 2; nMeasures <= vNumMeasuresPlus; nMeasures++) {
ConceptMatrix[t][nMeasures] = a[nMeasures].qNum;
}
}
});
ConceptMatrixFirstClean = ConceptMatrixFirst.filter(onlyUnique);
if (nRows >= (vMaxLoops * 1000)) {
alert(vErrorMessage);
}
if (vNumDims == 2) {
// new array with unique values for 2nd dim
var SecondHeader = ConceptMatrixSecond.filter(onlyUnique);// second dimension concepts
ConceptMatrixRowElem = ConceptMatrixRowElem.filter(onlyUnique);// first dimension concepts
ConceptMatrixColElem = ConceptMatrixColElem.filter(onlyUnique);// dimension code for further selections
const eo = ConceptMatrixColElem.length;
let vLoopColsMeasures = 1;
ConceptMatrixColElemTable[0] = ConceptMatrixColElem[0];
for (let xx = 0; xx < eo; xx++) {
if (vSeparatorCols && xx > 0) {
ConceptMatrixColElemTable[vLoopColsMeasures] = ConceptMatrixColElem[xx];
vLoopColsMeasures++;
}
for (let xxx = 0; xxx < vNumMeasures; xxx++) {
ConceptMatrixColElemTable[vLoopColsMeasures] = ConceptMatrixColElem[xx];
vLoopColsMeasures++;
}
}
SecondHeaderLength = SecondHeader.length;
vNumMeasures2 = vNumMeasures * SecondHeaderLength;
let ConceptPos = 0;
let nMeas3 = 0;
let vHeaderIndex = 0;
let MeasurePos = 0;
for (let nPivotElems = 0; nPivotElems <= lastrow; nPivotElems++) {
ConceptMatrixPivot[nPivotElems] = new Array();
ConceptPos = ConceptMatrixFirstClean.indexOf(ConceptMatrix[nPivotElems][0]);
ConceptMatrixPivot[ConceptPos][0] = ConceptMatrix[nPivotElems][0];
for (let nMeas2 = 1; nMeas2 <= measure_count; nMeas2++) {
nMeas3 = nMeas2 + 1;
vHeaderIndex = (SecondHeader.indexOf(ConceptMatrix[nPivotElems][1]) + 1);
MeasurePos = (vHeaderIndex * measure_count) + (nMeas2 - measure_count);
ConceptMatrixPivot[ConceptPos][MeasurePos] = ConceptMatrix[nPivotElems][nMeas3];
}
}
}
if (nRows > (lastrow + 1) && nRows <= (vMaxLoops * 1000)) {
const requestPage = [
{
qTop: lastrow + 1,
qLeft: 0,
qWidth: 10, // should be # of columns
qHeight: Math.min(1000, nRows - lastrow)
}
];
component.backendApi.getData(requestPage).then(() => {
component.paint($element);
});
}
const properties = {
ArrayGetSelectedCount,
ConceptMatrix,
ConceptMatrixColElem,
ConceptMatrixColElemTable,
ConceptMatrixRowElem,
ConceptMatrixFirstClean,
ConceptMatrixPivot,
vHeaderColorText,
vFontFamily,
vHeaderColorSchema,
vExportToExcel,
vNumDims,
nMeasAux,
dimensionInfos,
vLetterSizeHeader,
vHeaderAlignText,
MeasuresFormat,
measure_count,
vExcelButtonCode,
sufixCells,
LabelsArray,
SecondHeader,
vSeparatorCols,
nSecond: SecondHeaderLength - 1,
nSecond2: SecondHeaderLength - 1,
vLetterSize,
ExtraLabelsArray,
dim_count,
vExtraLabel,
vNumMeasures,
vNumMeasures2,
lastrow,
measureInfos
};
// TODO: figure out a reasonable datastructure and use these for component
const transformedProperties = {
dimensions: [],
headers: LabelsArray,
headerOptions: {
colorSchema: vHeaderColorSchema,
textColor: vHeaderColorText
},
options: {
fontFamily: vFontFamily
}
};
return {
colors,
dimensionInfos,
measureInfos,
properties,
transformedProperties
};
return transformedProperties;
}
export default initialize;

View File

@@ -1,26 +1,24 @@
function StyleBuilder (state) {
function StyleBuilder (styling) {
const {
CustomArray,
CustomArrayBasic,
vNumCustomHeaders,
vColorSchema,
vColorText,
vColorSchemaP,
vLetterSize,
colors
} = state;
customCSV,
options
} = styling;
let style = {
fontSize: `${14 + vLetterSize}px`
fontSize: `${14 + options.fontSizeAdjustment}px`
};
let hasComments = false;
let commentColor;
let hasCustomFileStyle = false;
function applyStandardAttributes (rowNumber) {
const isEven = rowNumber % 2 === 0;
style.backgroundColor = isEven ? vColorSchema : vColorSchemaP;
style.color = vColorText;
style.fontSize = `${14 + vLetterSize}px`;
const hasBackgroundColor = options.backgroundColor && options.backgroundColor.color;
const hasOddBackgroundColor = options.backgroundColorOdd && options.backgroundColorOdd.color;
if (hasBackgroundColor && hasOddBackgroundColor) {
const isEven = rowNumber % 2 === 0;
style.backgroundColor = isEven ? options.backgroundColor.color : options.backgroundColorOdd.color;
style.color = options.color;
}
style.fontSize = `${13 + options.fontSizeAdjustment}px`;
}
function applyColor (color) {
@@ -28,35 +26,28 @@ function StyleBuilder (state) {
commentColor = color;
}
/* eslint-disable sort-keys*/
const properties = {
'<comment>': () => { hasComments = true; },
// text
'<bold>': () => { style.fontWeight = 'bold'; },
'<italic>': () => { style.fontStyle = 'italic'; },
'<oblique>': () => { style.fontStyle = 'oblique'; },
// background and comment color
'<dark>': () => applyColor(colors.vColLibDark),
'<night>': () => applyColor(colors.vColLibNight),
'<soft>': () => applyColor(colors.vColLibSoft),
'<red>': () => applyColor(colors.vColLibRed),
'<orange>': () => applyColor(colors.vColLibOrange),
'<violete>': () => applyColor(colors.vColLibViolete),
'<blue>': () => applyColor(colors.vColLibBlue),
'<green>': () => applyColor(colors.vColLibGreen),
// font color TODO: this is a color just like the others, but it applies to text instead.. any way to make it less weird?
// font color
'<white>': () => { style.color = 'white'; },
// font size
'<large>': () => { style.fontSize = `${15 + vLetterSize}px`; },
'<medium>': () => { style.fontSize = `${14 + vLetterSize}px`; },
'<small>': () => { style.fontSize = `${13 + vLetterSize}px`; },
'<large>': () => { style.fontSize = `${15 + options.fontSizeAdjustment}px`; },
'<medium>': () => { style.fontSize = `${14 + options.fontSizeAdjustment}px`; },
'<small>': () => { style.fontSize = `${13 + options.fontSizeAdjustment}px`; },
// text alignment
'<center>': () => { style.textAlign = 'center'; }
};
/* eslint-enable sort-keys */
// TODO: need to improve this, it has way too many false positives
function isCSSColor (property) {
const isHexColor = property.substring(0, 1) == '#';
const isRGBColor = property.substring(0, 3).toUpperCase() == 'RGB';
const isHexColor = property.substring(0, 1) === '#';
const isRGBColor = property.substring(0, 3).toUpperCase() === 'RGB';
return isHexColor || isRGBColor;
}
@@ -84,27 +75,27 @@ function StyleBuilder (state) {
}
function parseCustomFileStyle (columnText) {
hasCustomFileStyle = true;
for (let csvAttribute = 1; csvAttribute < vNumCustomHeaders; csvAttribute += 1) {
for (let csvAttribute = 1; csvAttribute < customCSV.count; csvAttribute += 1) {
let customAttribute = '';
if (CustomArrayBasic.indexOf(columnText) < 0) {
if (customCSV.basic.indexOf(columnText) < 0) {
customAttribute = 'none';
} else {
customAttribute = CustomArray[CustomArrayBasic.indexOf(columnText)][csvAttribute];
hasCustomFileStyle = true;
customAttribute = customCSV.full[customCSV.basic.indexOf(columnText)][csvAttribute];
}
applyProperty(customAttribute);
}
}
return {
applyCustomStyle,
applyProperty,
applyStandardAttributes,
getCommentColor: () => commentColor,
getStyle: () => style,
hasComments: () => hasComments,
hasCustomFileStyle: () => hasCustomFileStyle,
hasFontSize: () => Boolean(style.fontSize),
hasComments: () => hasComments,
applyStandardAttributes,
applyProperty,
applyCustomStyle,
parseCustomFileStyle
};
}

65
src/tooltip/index.jsx Normal file
View File

@@ -0,0 +1,65 @@
import React from 'react';
import PropTypes from 'prop-types';
const handleCalculateTooltipPosition = (event) => {
const tooltip = document.querySelector('.tooltip-wrapper');
if (!tooltip) {
return;
}
tooltip.style.left = `${event.clientX}px`;
tooltip.style.top = `${event.clientY}px`;
};
class Tooltip extends React.PureComponent {
constructor (props) {
super(props);
this.state = {
showTooltip: false
};
this.handleRenderTooltip = this.handleRenderTooltip.bind(this);
}
handleRenderTooltip () {
const { showTooltip } = this.state;
this.setState({ showTooltip: !showTooltip });
}
render () {
const { children, styling, tooltipText } = this.props;
const { showTooltip } = this.state;
return (
<div
onMouseMove={handleCalculateTooltipPosition}
onMouseOut={this.handleRenderTooltip}
onMouseOver={this.handleRenderTooltip}
style={{ fontFamily: styling.options.fontFamily }}
>
{children}
{showTooltip
? (
<div className="tooltip-wrapper">
<p style={{ fontFamily: styling.options.fontFamily }}>
{tooltipText}
</p>
</div>
) : null}
</div>
);
}
}
Tooltip.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired,
styling: PropTypes.shape({
options: PropTypes.shape({
fontFamily: PropTypes.string.isRequired
}).isRequired
}).isRequired,
tooltipText: PropTypes.string.isRequired
};
export default Tooltip;

View File

@@ -2,17 +2,26 @@ export function onlyUnique (value, index, self) {
return self.indexOf(value) === index;
}
export function addSeparators (nStr, thousandsSep, decimalSep, numDecimals) {
let x1;
nStr = nStr.toFixed(numDecimals);
const x = nStr.split('.');
x1 = x[0];
const x2 = x.length > 1 ? decimalSep + x[1] : '';
const rgx = /(\d+)(\d{3})/;
while (rgx.test(x1)) {
x1 = x1.replace(rgx, `$1${thousandsSep}$2`);
export function distinctArray (array) {
return array
.map(entry => JSON.stringify(entry))
.filter(onlyUnique)
.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 x1 + x2;
return wholeNumber + decimal;
}
export function Deferred () {
@@ -21,3 +30,25 @@ export function Deferred () {
this.reject = reject;
});
}
export function injectSeparators (array, shouldHaveSeparator, suppliedOptions) {
const defaultOptions = {
atEvery: 1,
separator: { isSeparator: true }
};
const options = {
...defaultOptions,
...suppliedOptions
};
if (!shouldHaveSeparator) {
return array;
}
return array.reduce((result, entry, index) => {
result.push(entry);
if (index < array.length - 1 && (index + 1) % options.atEvery === 0) {
result.push(options.separator);
}
return result;
}, []);
}

View File

@@ -1,35 +1,129 @@
"use strict";
module.exports = {
rules: {
"at-rule-no-unknown": true,
"block-no-empty": true,
"color-no-invalid-hex": true,
"comment-no-empty": true,
"declaration-block-no-duplicate-properties": [
true,
'rules': {
'at-rule-empty-line-before': [
'always',
{
ignore: ["consecutive-duplicates-with-different-values"]
except: [
'blockless-after-same-name-blockless',
'first-nested'
],
ignore: ['after-comment']
}
],
"declaration-block-no-shorthand-property-overrides": true,
"font-family-no-duplicate-names": true,
"font-family-no-missing-generic-family-keyword": true,
"function-calc-no-unspaced-operator": true,
"function-linear-gradient-no-nonstandard-direction": true,
"keyframe-declaration-no-important": true,
"media-feature-name-no-unknown": true,
"no-descending-specificity": true,
"no-duplicate-at-import-rules": true,
"no-duplicate-selectors": true,
"no-empty-source": true,
"no-extra-semicolons": true,
"no-invalid-double-slash-comments": true,
"property-no-unknown": true,
"selector-pseudo-class-no-unknown": true,
"selector-pseudo-element-no-unknown": true,
"selector-type-no-unknown": true,
"string-no-newline": true,
"unit-no-unknown": true
'at-rule-name-case': 'lower',
'at-rule-name-space-after': 'always-single-line',
'at-rule-semicolon-newline-after': 'always',
'block-closing-brace-empty-line-before': 'never',
'block-closing-brace-newline-after': 'always',
'block-closing-brace-newline-before': 'always-multi-line',
'block-closing-brace-space-before': 'always-single-line',
'block-opening-brace-newline-after': 'always-multi-line',
'block-opening-brace-space-after': 'always-single-line',
'block-opening-brace-space-before': 'always',
'color-hex-case': 'lower',
'color-hex-length': 'short',
'color-named': 'never',
'comment-empty-line-before': [
'always',
{
except: ['first-nested'],
ignore: ['stylelint-commands']
}
],
'comment-whitespace-inside': 'always',
'custom-property-empty-line-before': [
'always',
{
except: [
'after-custom-property',
'first-nested'
],
ignore: [
'after-comment',
'inside-single-line-block'
]
}
],
'declaration-bang-space-after': 'never',
'declaration-bang-space-before': 'always',
'declaration-block-semicolon-newline-after': 'always-multi-line',
'declaration-block-semicolon-space-after': 'always-single-line',
'declaration-block-semicolon-space-before': 'never',
'declaration-block-single-line-max-declarations': 1,
'declaration-block-trailing-semicolon': 'always',
'declaration-colon-newline-after': 'always-multi-line',
'declaration-colon-space-after': 'always-single-line',
'declaration-colon-space-before': 'never',
'declaration-empty-line-before': [
'always',
{
except: [
'after-declaration',
'first-nested'
],
ignore: [
'after-comment',
'inside-single-line-block'
]
}
],
'declaration-no-important': [
true,
{
severity: 'warning'
}
],
'function-comma-newline-after': 'always-multi-line',
'function-comma-space-after': 'always-single-line',
'function-comma-space-before': 'never',
'function-max-empty-lines': 0,
'function-name-case': 'lower',
'function-parentheses-newline-inside': 'always-multi-line',
'function-parentheses-space-inside': 'never-single-line',
'function-whitespace-after': 'always',
'indentation': 2,
'length-zero-no-unit': true,
'max-empty-lines': 1,
'max-nesting-depth': 5,
'media-feature-colon-space-after': 'always',
'media-feature-colon-space-before': 'never',
'media-feature-name-case': 'lower',
'media-feature-parentheses-space-inside': 'never',
'media-feature-range-operator-space-after': 'always',
'media-feature-range-operator-space-before': 'always',
'media-query-list-comma-newline-after': 'always-multi-line',
'media-query-list-comma-space-after': 'always-single-line',
'media-query-list-comma-space-before': 'never',
'no-extra-semicolons': true,
'no-missing-end-of-source-newline': true,
'number-leading-zero': 'always',
'number-no-trailing-zeros': true,
'property-case': 'lower',
'rule-empty-line-before': [
'always-multi-line',
{
except: ['first-nested'],
ignore: ['after-comment']
}
],
'selector-attribute-brackets-space-inside': 'never',
'selector-attribute-operator-space-after': 'never',
'selector-attribute-operator-space-before': 'never',
'selector-combinator-space-after': 'always',
'selector-combinator-space-before': 'always',
'selector-descendant-combinator-no-non-space': true,
'selector-list-comma-newline-after': 'always',
'selector-list-comma-space-before': 'never',
'selector-max-empty-lines': 0,
'selector-pseudo-class-case': 'lower',
'selector-pseudo-class-parentheses-space-inside': 'never',
'selector-pseudo-element-case': 'lower',
'selector-pseudo-element-colon-notation': 'double',
'selector-type-case': 'lower',
'unit-case': 'lower',
'value-list-comma-newline-after': 'always-multi-line',
'value-list-comma-space-after': 'always-single-line',
'value-list-comma-space-before': 'never',
'value-list-max-empty-lines': 0
}
};

View File

@@ -1,20 +1,15 @@
const CopyWebpackPlugin = require('copy-webpack-plugin');
const StyleLintPlugin = require('stylelint-webpack-plugin');
const settings = require('./settings');
const packageJSON = require('./package.json');
const path = require('path');
console.log('Webpack mode:', settings.mode); // eslint-disable-line no-console
const DIST = path.resolve("./dist");
const MODE = process.env.NODE_ENV || 'development';
console.log('Webpack mode:', MODE); // eslint-disable-line no-console
const config = {
devtool: 'source-map',
entry: [
'./src/index.js'
],
mode: settings.mode,
output: {
path: settings.buildDestination,
filename: settings.name + '.js',
libraryTarget: 'amd'
},
entry: ['./src/index.js'],
externals: {
jquery: {
amd: 'jquery',
@@ -22,29 +17,35 @@ const config = {
commonjs2: 'jquery',
root: '_'
},
qlik: {
amd: 'qlik',
commonjs: 'qlik',
commonjs2: 'qlik',
root: '_'
}
},
// TODO: breaks core-js for some reason
// resolve: {
// extensions: ['js', 'jsx']
// },
mode: MODE,
module: {
rules: [
{
enforce: 'pre',
test: /\.(js|jsx)$/,
exclude: /(node_modules|Library)/,
loader: 'eslint-loader',
options: {
failOnError: true
}
},
test: /\.(js|jsx)$/
},
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
test: /\.(js|jsx)$/,
use: {
loader: 'babel-loader',
options: {
plugins: ['@babel/plugin-transform-async-to-generator'],
plugins: [
'@babel/plugin-transform-async-to-generator',
'@babel/plugin-proposal-class-properties'
],
presets: [
'@babel/preset-env',
'@babel/preset-react'
@@ -54,23 +55,23 @@ const config = {
},
{
test: /.less$/,
use: ['style-loader', 'css-loader', 'less-loader']
use: [
'style-loader',
'css-loader',
'less-loader'
]
}
]
},
output: {
filename: `${packageJSON.name}.js`,
libraryTarget: 'amd',
path: DIST
},
plugins: [
new CopyWebpackPlugin([
'assets/' + settings.name + '.qext',
'assets/' + settings.name + '.png',
'assets/wbfolder.wbl',
// TODO: remove entries below this line
'resources/Accounts.csv',
'resources/Accounts2.csv',
'resources/QlikLook.csv',
'resources/Excel.png',
], {}),
new StyleLintPlugin()
new StyleLintPlugin({
files: '**/*.less'
})
]
};