Compare commits

...

45 Commits

Author SHA1 Message Date
Albert Backenhof
b3b17e8d0c Merge pull request #58 from qlik-oss/DEB-173/MasterObject
Fixed master object bug
2019-05-03 09:28:27 +02:00
Albert Backenhof
5d45f57e00 Fixed master object bug
-Previously the smart pivot didn't render as
 a master object.

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

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

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

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

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

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

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

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

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

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

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

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

Issue: DEB-154
2019-04-09 07:50:33 +02:00
Albert Backenhof
629821bd6b The header width should match the table format
Issue: DEB-152
2019-04-08 10:39:02 +02:00
Albert Backenhof
08c5cf8104 Merge pull request #34 from qlik-oss/DEB-130/VersionInDesc
Aligned build to how Dashboard bundle extensions
2019-04-05 14:40:13 +02:00
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
42 changed files with 824 additions and 3445 deletions

View File

@@ -26,9 +26,6 @@ jobs:
--blackduck.username="svc-blackduck" \
--blackduck.password=${svc_blackduck} \
--detect.project.name="viz-bundle-qlik-smart-pivot"
- run:
name: Run tests
command: npm run test-once
bump-version:
<<: *defaults
@@ -56,16 +53,18 @@ jobs:
command: |
export VERSION=$(scripts/get-bumped-version.sh)
echo "Version: ${VERSION}"
npm run build
npm run build:zip
sudo chmod +x scripts/verify-files.sh
scripts/verify-files.sh
environment:
NODE_ENV: production
- persist_to_workspace:
root: ~/qlik-smart-pivot
paths:
- build
- dist
- store_artifacts:
path: build
destination: build
path: dist
destination: dist
deploy:
<<: *defaults
steps:

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

View File

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

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,69 +0,0 @@
const path = require('path');
const settings = require('./settings');
module.exports = (config) => {
config.set({
browsers: ['SlimChromeHeadless'],
customLaunchers: {
SlimChromeHeadless: {
base: 'ChromeHeadless',
flags: [
'--headless',
'--disable-gpu',
'--disable-translate',
'--disable-extensions'
]
}
},
files: [
{
pattern: 'src/**/*.spec.js',
watched: true
}
],
frameworks: ['jasmine'],
preprocessors: {
'src/**/*.spec.{js, jsx}': [
'webpack',
'sourcemap'
]
},
webpack: {
devtool: 'source-map',
externals: {
jquery: {
amd: 'jquery',
commonjs: 'jquery',
commonjs2: 'jquery',
root: '_'
}
},
mode: settings.mode,
module: {
rules: [
{
exclude: [/node_modules/],
loader: 'babel-loader',
options: {
plugins: [
'@babel/plugin-transform-async-to-generator',
'@babel/plugin-proposal-class-properties'],
presets: ['@babel/preset-react']
},
test: /\.(js|jsx)$/
},
{
loader: 'ignore-loader',
test: /\.less$/
}
]
},
resolve: {
alias: {
'test-utilities': path.resolve('test/test-utilities')
},
modules: ['node_modules']
}
}
});
};

2633
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,15 @@
{
"name": "qlik-smart-pivot",
"version": "0.0.1",
"description": "Formatted table for P&L reports.",
"keywords": "smart pivot qliksense extension",
"description": "Profit & Loss reporting with color and font customizations.",
"homepage": "",
"repository": "https://github.com/iviasensio/PLSmartPivot",
"author": "Ivan Felipe Asensio <ivan.felipe@qlik.com>",
"license": "MIT",
"scripts": {
"build": "gulp build",
"build:zip": "gulp zip",
"eslint": "eslint src",
"eslint:fix": "eslint --fix src",
"test": "karma start karma.conf.js",
"test-once": "karma start karma.conf.js --single-run",
"watch": "gulp watch",
"stylelint": "stylelint src/main.less"
},
"devDependencies": {
@@ -24,7 +21,6 @@
"@babel/preset-react": "7.0.0",
"babel-eslint": "10.0.1",
"babel-loader": "8.0.4",
"copy-webpack-plugin": "4.5.3",
"css-loader": "1.0.0",
"del": "2.0.2",
"enzyme": "3.8.0",
@@ -33,15 +29,8 @@
"eslint-loader": "2.1.1",
"eslint-plugin-react": "7.11.1",
"gulp": "4.0.0",
"gulp-json-editor": "2.4.3",
"gulp-util": "^3.0.7",
"gulp-zip": "3.0.2",
"jasmine-core": "3.2.1",
"jasmine-enzyme": "7.0.1",
"karma": "3.0.0",
"karma-chrome-launcher": "2.2.0",
"karma-jasmine": "1.1.2",
"karma-sourcemap-loader": "0.3.7",
"karma-webpack": "3.0.5",
"less": "3.8.1",
"less-loader": "4.1.0",
"lodash.merge": "4.6.1",
@@ -49,9 +38,7 @@
"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

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

@@ -33,40 +33,41 @@ class DataCell extends React.PureComponent {
styling
} = this.props;
const isColumnPercentageBased = (/%/).test(measurement.format);
let formattedMeasurementValue = formatMeasurementValue(measurement, styling);
if (styleBuilder.hasComments()) {
formattedMeasurementValue = '.';
}
let textAlignment = 'Right';
const textAlignmentProp = styling.options.textAlignment;
if (textAlignmentProp) {
textAlignment = textAlignmentProp;
}
let textAlignment = styling.options.textAlignment || 'Right';
let cellStyle = {
fontFamily: styling.options.fontFamily,
...styleBuilder.getStyle(),
paddingLeft: '5px',
textAlign: textAlignment
};
const { semaphoreColors, semaphoreColors: { fieldsToApplyTo } } = styling;
const isValidSemaphoreValue = !styleBuilder.hasComments() && !isNaN(measurement.value);
const dimension1Row = measurement.parents.dimension1.elementNumber;
const isSpecifiedMetricField = fieldsToApplyTo.metricsSpecificFields.indexOf(dimension1Row) !== -1;
const shouldHaveSemaphoreColors = (fieldsToApplyTo.applyToMetric || isSpecifiedMetricField);
if (isValidSemaphoreValue && shouldHaveSemaphoreColors) {
const { backgroundColor, color } = getSemaphoreColors(measurement, semaphoreColors);
cellStyle = {
...styleBuilder.getStyle(),
backgroundColor,
color,
fontFamily: styling.options.fontFamily,
paddingLeft: '5px',
textAlign: textAlignment
};
const isEmptyCell = measurement.displayValue === '';
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';
@@ -79,10 +80,11 @@ class DataCell extends React.PureComponent {
return (
<td
className={`${cellClass}${general.cellSuffix}`}
onClick={this.handleSelect}
onClick={isEmptyCell ? null : this.handleSelect}
style={cellStyle}
>
<Tooltip
styling={styling}
tooltipText={formattedMeasurementValue}
>
{formattedMeasurementValue}
@@ -174,12 +176,12 @@ function formatMeasurementValue (measurement, styling) {
return formattedMeasurementValue;
}
function getSemaphoreColors (measurement, semaphoreColors) {
if (measurement.value < semaphoreColors.status.critical) {
return semaphoreColors.statusColors.critical;
function getConditionalColor (measurement, conditionalColoring) {
if (measurement.value < conditionalColoring.threshold.poor) {
return conditionalColoring.colors.poor;
}
if (measurement.value < semaphoreColors.status.medium) {
return semaphoreColors.statusColors.medium;
if (measurement.value < conditionalColoring.threshold.fair) {
return conditionalColoring.colors.fair;
}
return semaphoreColors.statusColors.normal;
return conditionalColoring.colors.good;
}

View File

@@ -40,13 +40,15 @@ const DataTable = ({ data, general, qlik, renderData, styling }) => {
return (
<tr key={dimensionEntry.displayValue}>
<RowHeader
entry={dimensionEntry}
qlik={qlik}
rowStyle={rowStyle}
styleBuilder={styleBuilder}
styling={styling}
/>
{!renderData ?
<RowHeader
entry={dimensionEntry}
qlik={qlik}
rowStyle={rowStyle}
styleBuilder={styleBuilder}
styling={styling}
/> : null
}
{renderData && injectSeparators(
matrix[dimensionIndex],
styling.useSeparatorColumns,
@@ -69,6 +71,7 @@ const DataTable = ({ data, general, qlik, renderData, styling }) => {
</td>
);
}
const { dimension1: dimension1Info, dimension2, measurement } = measurementData.parents;
const id = `${dimension1Info.elementNumber}-${dimension2 && dimension2.elementNumber}-${measurement.header}`;
return (

View File

@@ -27,6 +27,7 @@ class RowHeader extends React.PureComponent {
>
<Tooltip
isTooltipActive={!inEditState}
styling={styling}
tooltipText={entry.displayValue}
>
<HeaderPadding

View File

@@ -6,7 +6,7 @@ function createCube (definition, app) {
});
}
async function buildDataCube (originCubeDefinition, dimensionIndexes, app) {
async function buildDataCube (originCubeDefinition, hasTwoDimensions, app) {
const cubeDefinition = {
...originCubeDefinition,
qInitialDataFetch: [
@@ -15,77 +15,50 @@ async function buildDataCube (originCubeDefinition, dimensionIndexes, app) {
qWidth: 10
}
],
qDimensions: [originCubeDefinition.qDimensions[dimensionIndexes.dimension1]],
qDimensions: [originCubeDefinition.qDimensions[0]],
qMeasures: originCubeDefinition.qMeasures
};
if (dimensionIndexes.dimension2) {
cubeDefinition.qDimensions.push(originCubeDefinition.qDimensions[dimensionIndexes.dimension2]);
if (hasTwoDimensions) {
cubeDefinition.qDimensions.push(originCubeDefinition.qDimensions[1]);
}
const cube = await createCube(cubeDefinition, app);
return cube.qHyperCube.qDataPages[0].qMatrix;
}
async function buildDesignCube (originCubeDefinition, dimensionIndexes, app) {
if (!dimensionIndexes.design) {
export async function initializeDataCube (component, layout) {
if (component.backendApi.isSnapshot) {
return layout.snapshotData.dataCube;
}
const app = qlik.currApp(component);
const properties = (await component.backendApi.getProperties());
// If this is a master object, fetch the hyperCubeDef of the original object
const hyperCubeDef = properties.qExtendsId
? (await app.getObjectProperties(properties.qExtendsId)).properties.qHyperCubeDef
: properties.qHyperCubeDef;
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;
}
const cube = await createCube({
qInitialDataFetch: [
{
qHeight: 1000,
qWidth: 1
}
],
qDimensions: [originCubeDefinition.qDimensions[dimensionIndexes.design]]
}, app);
return cube.qHyperCube.qDataPages[0].qMatrix;
}
const STYLE_SEPARATOR_COUNT = 7;
function findDesignDimension (qMatrix) {
return qMatrix[0].map(entry => (entry.qText.match(/;/g) || []).length).indexOf(STYLE_SEPARATOR_COUNT);
}
function getDimensionIndexes (dimensionsInformation, designDimensionIndex) {
const hasDesign = designDimensionIndex !== -1;
const nonDesignDimensionCount = hasDesign ? dimensionsInformation.length - 1 : dimensionsInformation.length;
const dimension1 = designDimensionIndex === 0 ? 1 : 0;
let dimension2 = false;
if (nonDesignDimensionCount === 2) {
dimension2 = hasDesign && designDimensionIndex < 2 ? 2 : 1;
}
const design = hasDesign && designDimensionIndex;
const firstMeasurementIndex = dimensionsInformation.length;
return {
design,
dimension1,
dimension2,
firstMeasurementIndex
};
}
export async function initializeCubes ({ component, layout }) {
const app = qlik.currApp(component);
const designDimensionIndex = findDesignDimension(layout.qHyperCube.qDataPages[0].qMatrix);
const dimensionsInformation = layout.qHyperCube.qDimensionInfo;
const dimensionIndexes = getDimensionIndexes(dimensionsInformation, designDimensionIndex);
let properties;
if (component.backendApi.isSnapshot) {
// Fetch properties of source
properties = (await app.getObjectProperties(layout.sourceObjectId)).properties;
} else {
properties = await component.backendApi.getProperties();
}
const originCubeDefinition = properties.qHyperCubeDef;
const designCube = await buildDesignCube(originCubeDefinition, dimensionIndexes, app);
const dataCube = await buildDataCube(originCubeDefinition, dimensionIndexes, app);
return {
design: designCube,
data: dataCube
};
return new Promise(resolve => {
const app = qlik.currApp(component);
const stylingField = app.field(layout.stylingfield);
const listener = function () {
const data = stylingField.rows.map(row => row.qText);
stylingField.OnData.unbind(listener);
resolve(data);
};
stylingField.OnData.bind(listener);
stylingField.getData();
});
}

View File

@@ -1,96 +0,0 @@
const conceptSemaphores = {
items: {
AllConcepts: {
component: 'switch',
defaultValue: true,
label: 'All concepts affected',
options: [
{
label: 'On',
value: true
},
{
label: 'Off',
value: false
}
],
ref: 'allsemaphores',
type: 'boolean'
},
ConceptsAffected1: {
defaultValue: '',
ref: 'conceptsemaphore1',
show: data => !data.allsemaphores,
translation: 'Concept 1',
type: 'string'
},
ConceptsAffected2: {
defaultValue: '',
ref: 'conceptsemaphore2',
show: data => !data.allsemaphores,
translation: 'Concept 2',
type: 'string'
},
ConceptsAffected3: {
defaultValue: '',
ref: 'conceptsemaphore3',
show: data => !data.allsemaphores,
translation: 'Concept 3',
type: 'string'
},
ConceptsAffected4: {
defaultValue: '',
ref: 'conceptsemaphore4',
show: data => !data.allsemaphores,
translation: 'Concept 4',
type: 'string'
},
ConceptsAffected5: {
defaultValue: '',
ref: 'conceptsemaphore5',
show: data => !data.allsemaphores,
translation: 'Concept 5',
type: 'string'
},
ConceptsAffected6: {
defaultValue: '',
ref: 'conceptsemaphore6',
show: data => !data.allsemaphores,
translation: 'Concept 6',
type: 'string'
},
ConceptsAffected7: {
defaultValue: '',
ref: 'conceptsemaphore7',
show: data => !data.allsemaphores,
translation: 'Concept 7',
type: 'string'
},
ConceptsAffected8: {
defaultValue: '',
ref: 'conceptsemaphore8',
show: data => !data.allsemaphores,
translation: 'Concept 8',
type: 'string'
},
ConceptsAffected9: {
defaultValue: '',
ref: 'conceptsemaphore9',
show: data => !data.allsemaphores,
translation: 'Concept 9',
type: 'string'
},
// eslint-disable-next-line sort-keys
ConceptsAffected10: {
defaultValue: '',
ref: 'conceptsemaphore10',
show: data => !data.allsemaphores,
translation: 'Concept 10',
type: 'string'
}
},
label: 'Concept Semaphores',
type: 'items'
};
export default conceptSemaphores;

View File

@@ -0,0 +1,199 @@
const conditionalColoring = {
type: 'items',
label: 'Color by performance',
items: {
Enabled: {
ref: 'conditionalcoloring.enabled',
type: 'boolean',
label: 'Enabled',
component: 'switch',
defaultValue: false,
options: [
{
value: true,
label: 'On'
},
{
value: false,
label: 'Off'
}
]
},
ColorAllRows: {
ref: 'conditionalcoloring.colorall',
type: 'boolean',
label: 'Color all rows',
component: 'switch',
defaultValue: true,
options: [
{
value: true,
label: 'All rows'
},
{
value: false,
label: 'Specified rows'
}
],
show (data) {
return data.conditionalcoloring.enabled;
}
},
Rows: {
type: 'array',
ref: 'conditionalcoloring.rows',
label: 'Rows to color',
itemTitleRef: function (data) {
return data.rowname;
},
allowAdd: true,
allowRemove: true,
addTranslation: 'Add row to color',
items: {
Row: {
ref: 'rowname',
label: 'Name of row',
type: 'string',
defaultValue: ''
}
},
show (data) {
return data.conditionalcoloring.enabled && !data.conditionalcoloring.colorall;
}
},
ColorAllMeasures: {
ref: 'conditionalcoloring.colorallmeasures',
type: 'boolean',
label: 'Color all measures',
component: 'switch',
defaultValue: true,
options: [
{
value: true,
label: 'All measures'
},
{
value: false,
label: 'Specified measures'
}
],
show (data) {
return data.conditionalcoloring.enabled;
}
},
Measures: {
ref: 'conditionalcoloring.measures',
translation: 'Measures by index (ex: 0,3)',
type: 'string',
defaultValue: '',
show (data) {
return data.conditionalcoloring.enabled
&& data.conditionalcoloring.colorallmeasures === false;
}
},
ThresholdPoor: {
ref: 'conditionalcoloring.threshold_poor',
translation: 'Poor range limit',
type: 'number',
defaultValue: -0.1,
show (data) {
return data.conditionalcoloring.enabled;
}
},
ColorPoor: {
ref: 'conditionalcoloring.color_poor',
label: 'Poor background color',
type: 'object',
component: 'color-picker',
dualOutput: true,
defaultValue: {
index: 10,
color: '#f93f17'
},
show (data) {
return data.conditionalcoloring.enabled;
}
},
TextColorPoor: {
ref: 'conditionalcoloring.textcolor_poor',
label: 'Poor text color',
type: 'object',
component: 'color-picker',
dualOutput: true,
defaultValue: {
index: 1,
color: '#ffffff'
},
show (data) {
return data.conditionalcoloring.enabled;
}
},
ThresholdFair: {
ref: 'conditionalcoloring.threshold_fair',
translation: 'Fair range limit',
type: 'number',
defaultValue: 0,
show (data) {
return data.conditionalcoloring.enabled;
}
},
ColorFair: {
ref: 'conditionalcoloring.color_fair',
label: 'Fair background color',
type: 'object',
component: 'color-picker',
dualOutput: true,
defaultValue: {
index: 8,
color: '#ffcf02'
},
show (data) {
return data.conditionalcoloring.enabled;
}
},
TextColorFair: {
ref: 'conditionalcoloring.textcolor_fair',
label: 'Fair text color',
type: 'object',
component: 'color-picker',
dualOutput: true,
defaultValue: {
index: 15,
color: '#000000'
},
show (data) {
return data.conditionalcoloring.enabled;
}
},
ColorGood: {
ref: 'conditionalcoloring.color_good',
label: 'Good background color',
type: 'object',
component: 'color-picker',
dualOutput: true,
defaultValue: {
index: 3,
color: '#276e27'
},
show (data) {
return data.conditionalcoloring.enabled;
}
},
TextColorGood: {
ref: 'conditionalcoloring.textcolor_good',
label: 'Good text color',
type: 'object',
component: 'color-picker',
dualOutput: true,
defaultValue: {
index: 1,
color: '#ffffff'
},
show (data) {
return data.conditionalcoloring.enabled;
}
}
}
};
export default conditionalColoring;

View File

@@ -1,10 +1,10 @@
const header = {
type: 'items',
label: 'Header Format',
label: 'Header format',
items: {
Align: {
ref: 'HeaderAlign',
translation: 'Header Alignment',
translation: 'Header alignment',
type: 'number',
component: 'buttongroup',
options: [
@@ -29,14 +29,14 @@ const header = {
index: 6,
color: '#4477aa'
},
label: 'Background Header Color',
label: 'Background color',
ref: 'HeaderColorSchema',
type: 'object',
dualOutput: true
},
HeaderTextColor: {
ref: 'HeaderTextColorSchema',
label: 'Text Header Color',
label: 'Text color',
component: 'color-picker',
defaultValue: {
index: 1,
@@ -47,7 +47,7 @@ const header = {
},
HeaderFontSize: {
ref: 'lettersizeheader',
translation: 'Font Size',
translation: 'Font size',
type: 'number',
component: 'buttongroup',
options: [

View File

@@ -1,8 +1,7 @@
import pagination from './pagination';
import header from './header';
import tableFormat from './table-format';
import conceptSemaphores from './concept-semaphores';
import metricSemaphores from './metric-semaphores';
import conditionalColoring from './conditional-coloring';
const definition = {
component: 'accordion',
@@ -18,18 +17,37 @@ const definition = {
},
uses: 'data'
},
sorting: {
uses: 'sorting'
},
settings: {
items: {
ConceptSemaphores: conceptSemaphores,
Formatted: tableFormat,
Header: header,
MetricSemaphores: metricSemaphores,
ConditionalColoring: conditionalColoring,
Pagination: pagination
},
uses: 'settings'
},
sorting: {
uses: 'sorting'
about: {
component: 'items',
label: 'About',
items: {
header: {
label: 'P&L pivot',
style: 'header',
component: 'text'
},
paragraph1: {
label: `P&L pivot is a Qlik Sense 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,112 +0,0 @@
const metricSemaphores = {
type: 'items',
label: 'Metric Semaphores',
items: {
AllMetrics: {
ref: 'allmetrics',
type: 'boolean',
component: 'switch',
label: 'All metrics affected',
options: [
{
value: true,
label: 'On'
},
{
value: false,
label: 'Off'
}
],
defaultValue: false
},
MetricsAffected: {
ref: 'metricssemaphore',
translation: 'Metrics affected (1,2,4,...)',
type: 'string',
defaultValue: '-',
show (data) {
return !data.allmetrics;
}
},
MetricStatus1: {
ref: 'metricsstatus1',
translation: 'Critic is less than',
type: 'number',
defaultValue: -0.1
},
ColorStatus1: {
ref: 'colorstatus1',
label: 'Critic Color Fill',
type: 'object',
component: 'color-picker',
dualOutput: true,
defaultValue: {
index: 8,
color: '#f93f17'
}
},
ColorStatus1Text: {
ref: 'colorstatus1text',
label: 'Critic Color Text',
type: 'object',
component: 'color-picker',
dualOutput: true,
defaultValue: {
index: 11,
color: '#ffffff'
}
},
MetricStatus2: {
ref: 'metricsstatus2',
translation: 'Medium is less than',
type: 'number',
defaultValue: 0
},
ColorStatus2: {
ref: 'colorstatus2',
label: 'Medium Color Fill',
type: 'object',
component: 'color-picker',
dualOutput: true,
defaultValue: {
index: 9,
color: '#ffcf02'
}
},
ColorStatus2Text: {
ref: 'colorstatus2text',
label: 'Medium Color Text',
type: 'object',
component: 'color-picker',
dualOutput: true,
defaultValue: {
index: 12,
color: '#000000'
}
},
ColorStatus3: {
ref: 'colorstatus3',
label: 'Success Color Fill',
type: 'object',
component: 'color-picker',
dualOutput: true,
defaultValue: {
index: 10,
color: '#276e27'
}
},
ColorStatus3Text: {
ref: 'colorstatus3text',
label: 'Success Color Text',
type: 'object',
component: 'color-picker',
dualOutput: true,
defaultValue: {
index: 11,
color: '#ffffff'
}
}
}
};
export default metricSemaphores;

View File

@@ -6,7 +6,7 @@ const pagination = {
ref: 'maxloops',
type: 'number',
component: 'dropdown',
label: 'Max Pagination Loops',
label: 'Max pagination loops',
options: [
{
value: 1,
@@ -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,12 +1,53 @@
const qlik = window.require('qlik');
// fixes case for when there are 3 dimensions, missies the case with 1 design dimension and 1 data dimension
function hasDesignDimension (data) {
return data.qHyperCubeDef.qDimensions.length > 2;
}
function getFieldList () {
return new Promise(function (resolve) {
const app = qlik.currApp();
app.getList('FieldList').then(function (model) {
// Close the model to prevent any updates.
app.destroySessionObject(model.layout.qInfo.qId);
// This is a bit iffy, might be smarter to reject and handle empty lists on the props instead.
if (!model.layout.qFieldList.qItems) {
return resolve([]);
}
// Resolve an array with master objects.
return resolve(model.layout.qFieldList.qItems.map(function (item) {
return {
value: item.qName,
label: item.qName
};
}));
});
});
}
const tableFormat = {
type: 'items',
label: 'Table Format',
label: 'Table format',
items: {
StylingField: {
ref: 'stylingfield',
disabledRef: '',
type: 'string',
component: 'dropdown',
label: 'Style template field',
options: function () {
return getFieldList().then(function (items) {
items.unshift(
{
value: '',
label: 'None'
});
return items;
});
}
},
IndentBool: {
ref: 'indentbool',
type: 'boolean',
@@ -17,7 +58,7 @@ const tableFormat = {
SeparatorColumns: {
ref: 'separatorcols',
type: 'boolean',
label: 'Separator Columns',
label: 'Column separators',
defaultValue: false
},
rowEvenBGColor: {
@@ -48,7 +89,7 @@ const tableFormat = {
ref: 'BodyTextColorSchema',
type: 'string',
component: 'dropdown',
label: 'Text Body Color',
label: 'Text body color',
options: [
{
value: 'Black',
@@ -98,7 +139,7 @@ const tableFormat = {
ref: 'FontFamily',
type: 'string',
component: 'dropdown',
label: 'FontFamily',
label: 'Font family',
options: [
{
value: 'QlikView Sans, -apple-system, sans-serif',
@@ -133,7 +174,7 @@ const tableFormat = {
},
DataFontSize: {
ref: 'lettersize',
translation: 'Font Size',
translation: 'Font size',
type: 'number',
component: 'buttongroup',
options: [
@@ -150,7 +191,7 @@ const tableFormat = {
},
textAlignment: {
ref: 'cellTextAlignment',
label: 'Cell Text alignment',
label: 'Cell text alignment',
component: 'buttongroup',
options: [
{
@@ -171,7 +212,7 @@ const tableFormat = {
ColumnWidthSlider: {
type: 'number',
component: 'slider',
label: 'Column Width',
label: 'Column width',
ref: 'columnwidthslider',
min: 1,
max: 3,
@@ -205,7 +246,7 @@ const tableFormat = {
ref: 'filteroncellclick',
type: 'boolean',
component: 'switch',
label: 'Filter data when cell clicked',
label: 'Allow selection in cells',
options: [
{
value: true,

View File

@@ -1,19 +1,32 @@
function removeAllTooltips (node) {
const tooltips = node.querySelectorAll('.tooltip');
[].forEach.call(tooltips, tooltip => {
if (tooltip.parentNode) {
tooltip.parentNode.removeChild(tooltip);
function cleanupNodes (node) {
const removables = node.querySelectorAll('.tooltip,input');
[].forEach.call(removables, removeable => {
if (removeable.parentNode) {
removeable.parentNode.removeChild(removeable);
}
});
}
function buildTableHTML (title, subtitle, footnote) {
function buildTableHTML (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"><i>Note:</i>${footnote}</p>`;
const dataTableClone = document.querySelector('.data-table').cloneNode(true);
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);
removeAllTooltips(dataTableClone);
const kpiTableBodies = kpiTableClone.querySelectorAll('tbody');
const dataTableBodies = dataTableClone.querySelectorAll('tbody');
const kpiHeader = kpiTableBodies[0].querySelector('tr');
const dataTableHeaders = dataTableBodies[0].querySelectorAll('tr');
const kpiRows = kpiTableBodies[1].querySelectorAll('tr');
const dataRows = dataTableBodies[1].querySelectorAll('tr');
let combinedRows = '';
for (let i = 0; i < kpiRows.length; i++) {
combinedRows += `<tr>${kpiRows[i].innerHTML}${dataRows[i].innerHTML}</tr>`;
}
const tableHTML = `
<html
@@ -41,8 +54,23 @@ function buildTableHTML (title, subtitle, footnote) {
<body>
${titleHTML.length > 0 ? titleHTML : ''}
${subtitleHTML.length > 0 ? subtitleHTML : ''}
<div>
<table>
<tbody>
<tr>
${kpiHeader.innerHTML}
${dataTableHeaders[0].innerHTML}
</tr>
${dataTableHeaders.length > 1 ? dataTableHeaders[1].outerHTML : ''}
</tbody>
</table>
<table>
<tbody>
${combinedRows}
</tbody>
</table>
</div>
${footnoteHTML.length > 0 ? footnoteHTML : ''}
${dataTableClone.outerHTML}
</body>
</html>
`.split('>.<')
@@ -55,15 +83,15 @@ function buildTableHTML (title, subtitle, footnote) {
function downloadXLS (html) {
const filename = 'analysis.xls';
const blobObject = new Blob([html]);
// IE/Edge
if (window.navigator.msSaveOrOpenBlob) {
const blobObject = new Blob([html]);
return window.navigator.msSaveOrOpenBlob(blobObject, filename);
}
const dataURI = generateDataURI(html);
const link = window.document.createElement('a');
link.href = dataURI;
link.href = URL.createObjectURL(blobObject);
link.download = filename;
document.body.appendChild(link);
link.click();
@@ -72,15 +100,8 @@ function downloadXLS (html) {
return true;
}
function generateDataURI (html) {
const dataType = 'data:application/vnd.ms-excel;base64,';
const data = window.btoa(unescape(encodeURIComponent(html)));
return `${dataType}${data}`;
}
export function exportXLS (title, subtitle, footnote) {
export function exportXLS (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(title, subtitle, footnote);
const table = buildTableHTML(id, title, subtitle, footnote);
downloadXLS(table);
}

View File

@@ -9,10 +9,10 @@ class ExportButton extends React.PureComponent {
}
handleExport () {
const { excelExport, general } = this.props;
const { id, excelExport, general } = this.props;
const { title, subtitle, footnote } = general;
if (excelExport) {
exportXLS(title, subtitle, footnote);
exportXLS(id, title, subtitle, footnote);
}
}
@@ -34,6 +34,7 @@ ExportButton.defaultProps = {
};
ExportButton.propTypes = {
id: PropTypes.string.isRequired,
excelExport: PropTypes.bool,
general: PropTypes.shape({}).isRequired
};

View File

@@ -1,5 +1,6 @@
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 {
@@ -16,11 +17,12 @@ class ColumnHeader extends React.PureComponent {
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: '45px',
height: isMediumFontSize ? '43px' : '33px',
verticalAlign: 'middle'
};
@@ -33,6 +35,7 @@ class ColumnHeader extends React.PureComponent {
>
<Tooltip
isTooltipActive={!inEditState}
styling={styling}
tooltipText={entry.displayValue}
>
{entry.displayValue}

View File

@@ -3,14 +3,14 @@ import PropTypes from 'prop-types';
import ExportButton from '../export-button.jsx';
import { HEADER_FONT_SIZE } from '../initialize-transformed';
const ExportColumnHeader = ({ baseCSS, general, title, allowExcelExport, hasSecondDimension, styling }) => {
const ExportColumnHeader = ({ 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 ? '100px' : '80px',
height: isMediumFontSize ? '90px' : '70px',
verticalAlign: 'middle',
width: '230px'
};
@@ -22,6 +22,7 @@ const ExportColumnHeader = ({ baseCSS, general, title, allowExcelExport, hasSeco
style={style}
>
<ExportButton
id={id}
excelExport={allowExcelExport}
general={general}
/>
@@ -31,6 +32,7 @@ const ExportColumnHeader = ({ baseCSS, general, title, allowExcelExport, hasSeco
};
ExportColumnHeader.propTypes = {
id: PropTypes.string.isRequired,
allowExcelExport: PropTypes.bool.isRequired,
baseCSS: PropTypes.shape({}).isRequired,
general: PropTypes.shape({}).isRequired,

View File

@@ -5,7 +5,7 @@ import ColumnHeader from './column-header.jsx';
import MeasurementColumnHeader from './measurement-column-header.jsx';
import { injectSeparators } from '../utilities';
const HeadersTable = ({ data, general, qlik, styling }) => {
const HeadersTable = ({ data, general, qlik, styling, isKpi }) => {
const baseCSS = {
backgroundColor: styling.headerOptions.colorSchema,
color: styling.headerOptions.textColor,
@@ -26,15 +26,18 @@ const HeadersTable = ({ data, general, qlik, styling }) => {
<table className="header">
<tbody>
<tr>
<ExportColumnHeader
allowExcelExport={general.allowExcelExport}
baseCSS={baseCSS}
general={general}
hasSecondDimension={hasSecondDimension}
styling={styling}
title={dimension1[0].name}
/>
{!hasSecondDimension && measurements.map(measurementEntry => (
{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}
@@ -44,7 +47,7 @@ const HeadersTable = ({ data, general, qlik, styling }) => {
styling={styling}
/>
))}
{hasSecondDimension && injectSeparators(dimension2, styling.useSeparatorColumns).map((entry, index) => {
{!isKpi && hasSecondDimension && injectSeparators(dimension2, styling.useSeparatorColumns).map((entry, index) => {
if (entry.isSeparator) {
const separatorStyle = {
color: 'white',
@@ -75,7 +78,7 @@ const HeadersTable = ({ data, general, qlik, styling }) => {
);
})}
</tr>
{hasSecondDimension && (
{!isKpi && hasSecondDimension && (
<tr>
{injectSeparators(dimension2, styling.useSeparatorColumns).map((dimensionEntry, index) => {
if (dimensionEntry.isSeparator) {
@@ -137,7 +140,8 @@ HeadersTable.propTypes = {
styling: PropTypes.shape({
headerOptions: PropTypes.shape({}),
options: PropTypes.shape({})
}).isRequired
}).isRequired,
isKpi: PropTypes.bool.isRequired
};
export default HeadersTable;

View File

@@ -6,7 +6,7 @@ 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 = styling.headerOptions.fontSizeAdjustment === HEADER_FONT_SIZE.MEDIUM;
const isMediumFontSize = fontSizeAdjustment === HEADER_FONT_SIZE.MEDIUM;
if (hasSecondDimension) {
const isPercentageFormat = measurement.format.substring(measurement.format.length - 1) === '%';
@@ -18,9 +18,8 @@ const MeasurementColumnHeader = ({ baseCSS, general, hasSecondDimension, measure
}
const cellStyle = {
...baseCSS,
cursor: 'default',
fontSize: `${baseFontSize + fontSizeAdjustment}px`,
height: isMediumFontSize ? '50px' : '25px',
height: isMediumFontSize ? '45px' : '35px',
verticalAlign: 'middle'
};
return (
@@ -28,37 +27,33 @@ const MeasurementColumnHeader = ({ baseCSS, general, hasSecondDimension, measure
className={`${cellClass}${general.cellSuffix}`}
style={cellStyle}
>
<span className="wrapclass25">
<Tooltip
tooltipText={title}
>
{title}
</Tooltip>
</span>
<Tooltip
tooltipText={title}
styling={styling}
>
{title}
</Tooltip>
</th>
);
}
const isLong = (title.length > 11 && fontSizeAdjustment === 0) || (title.length > 12 && fontSizeAdjustment === -2);
const suffixWrap = isLong ? '70' : 'empty';
const style = {
...baseCSS,
cursor: 'default',
fontSize: `${15 + fontSizeAdjustment}px`,
height: isMediumFontSize ? '100px' : '80px',
height: isMediumFontSize ? '90px' : '70px',
verticalAlign: 'middle'
};
return (
<th
className={`grid-cells2 ${general.cellSuffix}`}
className={`grid-cells2${general.cellSuffix}`}
style={style}
>
<span
className={`wrapclass${suffixWrap}`}
style={{ fontFamily: styling.headerOptions.fontFamily }}
<Tooltip
tooltipText={title}
styling={styling}
>
{title}
</span>
</Tooltip>
</th>
);
};

View File

@@ -1,5 +1,9 @@
import paint from './paint.jsx';
import definition from './definition';
import { initializeDataCube, initializeDesignList } from './dataset';
import initializeStore from './store';
import React from 'react';
import ReactDOM from 'react-dom';
import Root from './root.jsx';
import './main.less';
if (!window._babelPolyfill) { // eslint-disable-line no-underscore-dangle
@@ -20,12 +24,16 @@ export default {
},
data: {
dimensions: {
max: 3,
max: function (nMeasures) {
return nMeasures < 9 ? 2 : 1;
},
min: 1,
uses: 'dimensions'
},
measures: {
max: 8,
max: function (nDims) {
return nDims < 2 ? 9 : 8;
},
min: 1,
uses: 'measures'
}
@@ -49,16 +57,34 @@ export default {
exportData: true,
snapshot: true
},
paint ($element, layout) {
try {
paint($element, layout, this);
} catch (exception) {
console.error(exception); // eslint-disable-line no-console
throw exception;
}
paint: async function ($element, layout) {
const dataCube = await initializeDataCube(this, layout);
const designList = await initializeDesignList(this, layout);
const state = await initializeStore({
$element,
component: this,
dataCube,
designList,
layout
});
const editmodeClass = this.inAnalysisState() ? '' : 'edit-mode';
const jsx = (
<Root
editmodeClass={editmodeClass}
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

@@ -18,8 +18,7 @@ function getAlignment (option) {
function getFontSizeAdjustment (option) {
const fontSizeAdjustmentOptions = {
1: HEADER_FONT_SIZE.SMALL,
2: HEADER_FONT_SIZE.MEDIUM,
3: 2
2: HEADER_FONT_SIZE.MEDIUM
};
return fontSizeAdjustmentOptions[option] || 0;
@@ -68,7 +67,7 @@ function generateMeasurements (information) {
function generateDimensionEntry (information, data) {
return {
displayValue: data.qText,
displayValue: data.qText || data.qNum,
elementNumber: data.qElemNumber,
name: information.qFallbackTitle,
value: data.qNum
@@ -91,7 +90,8 @@ function generateMatrixCell ({ cell, dimension1Information, dimension2Informatio
header: dimension1Information.qText
},
measurement: {
header: measurementInformation.name
header: measurementInformation.name,
index: measurementInformation.index
}
},
value: cell.qNum
@@ -107,16 +107,16 @@ function generateMatrixCell ({ cell, dimension1Information, dimension2Informatio
}
let lastRow = 0;
function generateDataSet (component, dimensionsInformation, measurementsInformation, cubes) {
const dimension1 = [];
const dimension2 = [];
function generateDataSet (
component, dimensionsInformation, measurementsInformation, dataCube) {
const measurements = generateMeasurements(measurementsInformation);
let dimension1 = [];
let dimension2 = [];
let matrix = [];
let previousDim1Entry;
const hasDesignDimension = cubes.design;
const hasSecondDimension = hasDesignDimension ? dimensionsInformation.length > 2 : dimensionsInformation.length > 1;
cubes.data.forEach(row => {
const hasSecondDimension = dimensionsInformation.length > 1;
dataCube.forEach(row => {
lastRow += 1;
const dimension1Entry = generateDimensionEntry(dimensionsInformation[0], row[0]);
dimension1.push(dimension1Entry);
@@ -127,10 +127,11 @@ function generateDataSet (component, dimensionsInformation, measurementsInformat
dimension2.push(dimension2Entry);
firstDataCell = 2;
}
const matrixRow = row
let matrixRow = row
.slice(firstDataCell, row.length)
.map((cell, cellIndex) => {
const measurementInformation = measurements[cellIndex];
measurementInformation.index = cellIndex;
const dimension1Information = row[0]; // eslint-disable-line prefer-destructuring
const dimension2Information = hasSecondDimension ? row[1] : null;
const generatedCell = generateMatrixCell({
@@ -143,36 +144,92 @@ function generateDataSet (component, dimensionsInformation, measurementsInformat
return generatedCell;
});
let appendToRowIndex = matrix.length;
if (hasSecondDimension) {
const currentDim1Entry = row[0].qText;
const isSameDimension1AsPrevious = currentDim1Entry === previousDim1Entry;
if (isSameDimension1AsPrevious) {
const updatedRow = matrix[matrix.length - 1].concat(matrixRow);
matrix = [
...matrix.slice(0, matrix.length - 1),
updatedRow
];
} else {
matrix[matrix.length] = matrixRow;
// See if there already is a row for the current dim1
for (let i = 0; i < matrix.length; i++) {
if (matrix[i][0].parents.dimension1.header === matrixRow[0].parents.dimension1.header) {
appendToRowIndex = i;
matrixRow = matrix[i].concat(matrixRow);
}
}
previousDim1Entry = currentDim1Entry;
} else {
matrix[matrix.length] = matrixRow;
}
matrix[appendToRowIndex] = matrixRow;
});
// filter header dimensions to only have distinct values
dimension1 = distinctArray(dimension1);
dimension2 = distinctArray(dimension2);
// Make sure all rows are saturated, otherwise data risks being displayed in the wrong column
matrix = matrix.map((row, rowIndex) => {
if ((hasSecondDimension && row.length == (dimension2.length * measurements.length))
|| (!hasSecondDimension && row.length == measurements.length)) {
// Row is saturated
return row;
}
// Row is not saturated, so must add empty cells to fill the gaps
let newRow = [];
if (hasSecondDimension) {
// Got a second dimension, so need to add measurements for all values of the second dimension
let rowDataIndex = 0;
dimension2.forEach(dim => {
rowDataIndex = appendMissingCells(
row, newRow, rowDataIndex, measurements, rowIndex, dim.elementNumber);
});
} else {
appendMissingCells(row, newRow, 0, measurements, rowIndex);
}
return newRow;
});
return {
dimension1: distinctArray(dimension1),
dimension2: distinctArray(dimension2),
dimension1: dimension1,
dimension2: dimension2,
matrix,
measurements
};
}
function initializeTransformed ({ $element, component, cubes, layout }) {
/*
* Appends the cells of the source row, as well as those missing, to the destination row, starting
* from the given source index. Returns the source index of the next source cell after this has
* completed. If there is a second dimension the dim2ElementNumber should be set to the current
* index of the dimension2 value being processed.
*/
function appendMissingCells (
sourceRow, destRow, sourceIndex, measurements, 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;
@@ -183,19 +240,18 @@ function initializeTransformed ({ $element, component, cubes, layout }) {
dimension2,
measurements,
matrix
} = generateDataSet(component, dimensionsInformation, measurementsInformation, cubes);
} = generateDataSet(component, dimensionsInformation, measurementsInformation, dataCube);
const customSchemaBasic = [];
const customSchemaFull = [];
let customHeadersCount = 0;
if (cubes.design) {
const allTextLines = cubes.design.map(entry => entry[0].qText);
const headers = allTextLines[0].split(';');
if (designList && designList.length > 0) {
const headers = designList[0].split(';');
customHeadersCount = headers.length;
for (let lineNumber = 0; lineNumber < allTextLines.length; lineNumber += 1) {
for (let lineNumber = 0; lineNumber < designList.length; lineNumber += 1) {
customSchemaFull[lineNumber] = new Array(headers.length);
const data = allTextLines[lineNumber].split(';');
const data = designList[lineNumber].split(';');
if (data.length === headers.length) {
for (let headerIndex = 0; headerIndex < headers.length; headerIndex += 1) {
@@ -239,7 +295,7 @@ function initializeTransformed ({ $element, component, cubes, layout }) {
count: customHeadersCount,
full: customSchemaFull
},
hasCustomFileStyle: Boolean(cubes.design),
hasCustomFileStyle: Boolean(designList),
headerOptions: {
alignment: getAlignment(layout.HeaderAlign),
colorSchema: layout.HeaderColorSchema.color,
@@ -254,39 +310,30 @@ function initializeTransformed ({ $element, component, cubes, layout }) {
fontSizeAdjustment: getFontSizeAdjustment(layout.lettersize),
textAlignment: layout.cellTextAlignment
},
semaphoreColors: {
fieldsToApplyTo: {
applyToAll: layout.allsemaphores,
applyToMetric: layout.allmetrics,
specificFields: [
layout.conceptsemaphore1,
layout.conceptsemaphore2,
layout.conceptsemaphore3,
layout.conceptsemaphore4,
layout.conceptsemaphore5,
layout.conceptsemaphore6,
layout.conceptsemaphore7,
layout.conceptsemaphore9,
layout.conceptsemaphore10
],
metricsSpecificFields: layout.metricssemaphore.split(',').map(entry => Number(entry))
conditionalColoring: {
enabled: layout.conditionalcoloring.enabled,
colorAllRows: layout.conditionalcoloring.colorall,
rows: layout.conditionalcoloring.rows.map(row => row.rowname),
colorAllMeasures: typeof layout.conditionalcoloring.colorallmeasures === 'undefined'
|| layout.conditionalcoloring.colorallmeasures,
measures: !layout.conditionalcoloring.measures
? [] : layout.conditionalcoloring.measures.split(',').map(index => Number(index)),
threshold: {
poor: layout.conditionalcoloring.threshold_poor,
fair: layout.conditionalcoloring.threshold_fair
},
status: {
critical: layout.metricsstatus1,
medium: layout.metricsstatus2
},
statusColors: {
critical: {
backgroundColor: layout.colorstatus1.color,
color: layout.colorstatus1text.color
colors: {
poor: {
color: layout.conditionalcoloring.color_poor,
textColor: layout.conditionalcoloring.textcolor_poor
},
medium: {
backgroundColor: layout.colorstatus2.color,
color: layout.colorstatus2text.color
fair: {
color: layout.conditionalcoloring.color_fair,
textColor: layout.conditionalcoloring.textcolor_fair
},
normal: {
backgroundColor: layout.colorstatus3.color,
color: layout.colorstatus3text.color
good: {
color: layout.conditionalcoloring.color_good,
textColor: layout.conditionalcoloring.textcolor_good
}
}
},

View File

@@ -1,6 +1,5 @@
/* eslint-disable */
.qv-object-qlik-smart-pivot {
@TableBorder: 1px solid #d3d3d3;
@KpiTableWidth: 230px;
*,
@@ -33,18 +32,17 @@
}
table {
border-collapse: collapse;
border-spacing: 0;
border-collapse: separate;
border-spacing: 1px;
width: auto;
border-left: @TableBorder;
border-right: @TableBorder;
border-top: @TableBorder;
}
tr {
height: 25px;
}
td,
th {
border: 1px solid #fff;
border-collapse: collapse;
padding: 5px !important; // prevent overwriting from single object
white-space: nowrap;
overflow: hidden;
@@ -70,45 +68,12 @@
th.main-kpi {
text-align: center;
vertical-align: middle;
border-bottom: @TableBorder;
}
.numeric {
text-align: right;
}
// This is for wrap text in headers
.wrapclass25 {
width: 100%;
height: inherit;
white-space: pre-line;
overflow: hidden;
display: block;
text-overflow: ellipsis;
}
.wrapclass45 {
width: 100%;
height: 45px;
white-space: pre-line;
overflow: hidden;
display: block;
}
.wrapclass70 {
width: 100%;
height: 70px;
white-space: pre-line;
overflow: hidden;
display: inline-block;
vertical-align: middle;
line-height: 20px;
}
.wrapclassEmpty {
width: 100%;
}
// *****************
// Medium column size
// *****************
@@ -178,12 +143,7 @@
background-color: #fff;
}
.fdim-cells:hover {
background-color: #808080 !important;
color: #fff;
}
tbody tr:hover {
tbody tr:hover td {
cursor: default;
background-color: #808080 !important;
color: #fff;
@@ -210,33 +170,50 @@
border: groove;
}
.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 {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
flex: none;
width: @KpiTableWidth !important;
overflow: hidden !important;
height: 100%;
margin: 0;
padding: 0;
position: absolute;
top: 0;
left: 0;
border-right: 1px solid #fff;
box-shadow: 4px 2px 8px #e1e1e1;
.header-wrapper {
flex: none;
box-shadow: 4px 2px 8px #e1e1e1;
}
.row-wrapper {
height: calc(~"100% - 97px");
overflow: scroll;
position: absolute;
margin: 0;
margin-bottom: 8px;
padding: 0;
margin-top: 0;
box-shadow: 4px 2px 8px #e1e1e1;
min-height: 0; /* This is to make flex size-filling work */
/* Adapt for Edge */
@supports (-ms-ime-align: auto) {
margin-bottom: 16px;
}
/* Adapt for IE11 */
@media screen and (-ms-high-contrast: none) {
margin-bottom: 16px;
}
}
}
@@ -245,27 +222,50 @@
}
.data-table {
height: 100%;
width: calc(100% - 243px);
position: absolute;
margin-left: @KpiTableWidth + 13px;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
margin-left: 13px;
min-width: 0; /* This is to make flex size-filling work */
.header-wrapper {
flex: none;
overflow: scroll;
width: 100%;
margin-right: 8px;
}
.row-wrapper {
height: calc(~"100% - 97px");
width: 100%;
overflow: scroll;
margin: 0;
padding: 0;
margin-top: 0;
min-height: 0; /* This is to make flex size-filling work */
/* Style scrollbar for FF */
scrollbar-width: thin;
scrollbar-color: #d3d3d3 transparent;
}
/* Adapt for Edge */
@supports (-ms-ime-align: auto) {
.header-wrapper {
margin-right: 16px;
}
}
/* Adapt for IE11 */
@media screen and (-ms-high-contrast: none) {
width: 100%;
height: 100%;
.header-wrapper {
margin-right: 16px;
}
}
}
// hide scrollbars
.kpi-table .header-wrapper,
.kpi-table .row-wrapper,
.data-table .header-wrapper {
// stylelint-disable-next-line property-no-unknown
scrollbar-width: none;

View File

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

View File

@@ -5,46 +5,50 @@ import DataTable from './data-table/index.jsx';
import { LinkedScrollWrapper, LinkedScrollSection } from './linked-scroll';
const Root = ({ state, qlik, editmodeClass }) => (
<LinkedScrollWrapper>
<div className={`kpi-table ${editmodeClass}`}>
<HeadersTable
data={state.data}
general={state.general}
qlik={qlik}
styling={state.styling}
/>
<LinkedScrollSection linkVertical>
<DataTable
data={state.data}
general={state.general}
qlik={qlik}
renderData={false}
styling={state.styling}
/>
</LinkedScrollSection>
</div>
<div className={`data-table ${editmodeClass}`}>
<LinkedScrollSection linkHorizontal>
<div className="root">
<LinkedScrollWrapper>
<div className={`kpi-table ${editmodeClass}`}>
<HeadersTable
data={state.data}
general={state.general}
isKpi
qlik={qlik}
styling={state.styling}
/>
</LinkedScrollSection>
<LinkedScrollSection
linkHorizontal
linkVertical
>
<DataTable
data={state.data}
general={state.general}
qlik={qlik}
styling={state.styling}
/>
</LinkedScrollSection>
</div>
</LinkedScrollWrapper>
<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 = {

View File

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

View File

@@ -25,23 +25,21 @@ class Tooltip extends React.PureComponent {
}
render () {
const { children, tooltipText } = this.props;
const { children, styling, tooltipText } = this.props;
const { showTooltip } = this.state;
return (
<div
onMouseMove={handleCalculateTooltipPosition}
onMouseOut={this.handleRenderTooltip}
onMouseOver={this.handleRenderTooltip}
style={{ fontFamily: styling.options.fontFamily }}
>
{children}
{showTooltip
? (
<div
className="tooltip-wrapper"
>
<p>
<div className="tooltip-wrapper">
<p style={{ fontFamily: styling.options.fontFamily }}>
{tooltipText}
</p>
</div>
@@ -56,6 +54,11 @@ Tooltip.propTypes = {
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired,
styling: PropTypes.shape({
options: PropTypes.shape({
fontFamily: PropTypes.string.isRequired
}).isRequired
}).isRequired,
tooltipText: PropTypes.string.isRequired
};

View File

@@ -84,7 +84,7 @@ module.exports = {
'indentation': 2,
'length-zero-no-unit': true,
'max-empty-lines': 1,
'max-nesting-depth': 3,
'max-nesting-depth': 5,
'media-feature-colon-space-after': 'always',
'media-feature-colon-space-before': 'never',
'media-feature-name-case': 'lower',

File diff suppressed because one or more lines are too long

View File

@@ -1,10 +0,0 @@
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import jasmineEnzyme from 'jasmine-enzyme';
Enzyme.configure({ adapter: new Adapter() });
beforeEach(() => {
jasmineEnzyme();
});
export * from 'enzyme';

View File

@@ -1,9 +0,0 @@
import React from 'react';
import { mount } from 'test-utilities/enzyme-setup';
export function mountedComponent (Model, Component, props = {}) {
const component = mount((
<Component {...props} />
)).find(Component);
return new Model(component);
}

View File

@@ -1,8 +1,11 @@
const CopyWebpackPlugin = require('copy-webpack-plugin');
const StyleLintPlugin = require('stylelint-webpack-plugin');
const settings = require('./settings');
const packageJSON = require('./package.json');
const path = require('path');
console.log('Webpack mode:', settings.mode); // eslint-disable-line no-console
const DIST = path.resolve("./dist");
const MODE = process.env.NODE_ENV || 'development';
console.log('Webpack mode:', MODE); // eslint-disable-line no-console
const config = {
devtool: 'source-map',
@@ -21,7 +24,7 @@ const config = {
root: '_'
}
},
mode: settings.mode,
mode: MODE,
module: {
rules: [
{
@@ -61,17 +64,11 @@ const config = {
]
},
output: {
filename: `${settings.name}.js`,
filename: `${packageJSON.name}.js`,
libraryTarget: 'amd',
path: settings.buildDestination
path: DIST
},
plugins: [
new CopyWebpackPlugin([
`assets/${settings.name}.qext`,
`assets/${settings.name}.png`,
'assets/wbfolder.wbl',
'resources/Excel.png'
], {}),
new StyleLintPlugin({
files: '**/*.less'
})