Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f843779b64 | ||
|
|
808f4df3e3 | ||
|
|
47b4d1aa5b | ||
|
|
614d768eea | ||
|
|
555000be54 | ||
|
|
c367f24dd9 | ||
|
|
0b3b7b3f57 | ||
|
|
44b33b4c92 | ||
|
|
61b339b146 | ||
|
|
24edf1c6f4 | ||
|
|
fc363d7739 | ||
|
|
35d4dde118 | ||
|
|
e70e76a401 | ||
|
|
bcb9d30237 | ||
|
|
ec140efc56 | ||
|
|
34477d7ef1 | ||
|
|
b65d1c51fc | ||
|
|
9111ec762b | ||
|
|
b86806d4cd | ||
|
|
db67b864ee | ||
|
|
c3651a37da | ||
|
|
8b843e028a | ||
|
|
2bdd98aaca | ||
|
|
d723451656 | ||
|
|
c47b401a1d | ||
|
|
3c330465dd | ||
|
|
f2f201c6e2 | ||
|
|
8e1394e898 | ||
|
|
c69cfec533 | ||
|
|
c4a717bd77 | ||
|
|
b979a579f9 | ||
|
|
6233b9dbae | ||
|
|
9b682e62f8 |
@@ -18,6 +18,14 @@ jobs:
|
||||
- run:
|
||||
name: Install dependencies
|
||||
command: npm install
|
||||
- run:
|
||||
name: BlackDuck scan
|
||||
command: curl -s https://blackducksoftware.github.io/hub-detect/hub-detect.sh | bash -s -- \
|
||||
--blackduck.url="https://qliktech.blackducksoftware.com" \
|
||||
--blackduck.trust.cert=true \
|
||||
--blackduck.username="svc-blackduck" \
|
||||
--blackduck.password=${svc_blackduck} \
|
||||
--detect.project.name="viz-bundle-qlik-smart-pivot"
|
||||
- run:
|
||||
name: Run tests
|
||||
command: npm run test-once
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Smart pivot",
|
||||
"description": "Formatted table for P&L reports.",
|
||||
"name": "P&L pivot",
|
||||
"description": "Profit & Loss reporting with color and font customizations.",
|
||||
"type": "visualization",
|
||||
"version": "X.Y.Z",
|
||||
"icon": "table",
|
||||
|
||||
108
package-lock.json
generated
108
package-lock.json
generated
@@ -101,6 +101,104 @@
|
||||
"@babel/types": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"@babel/helper-create-class-features-plugin": {
|
||||
"version": "7.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.3.2.tgz",
|
||||
"integrity": "sha512-tdW8+V8ceh2US4GsYdNVNoohq5uVwOf9k6krjwW4E1lINcHgttnWcNqgdoessn12dAy8QkbezlbQh2nXISNY+A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-function-name": "^7.1.0",
|
||||
"@babel/helper-member-expression-to-functions": "^7.0.0",
|
||||
"@babel/helper-optimise-call-expression": "^7.0.0",
|
||||
"@babel/helper-plugin-utils": "^7.0.0",
|
||||
"@babel/helper-replace-supers": "^7.2.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/generator": {
|
||||
"version": "7.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.3.3.tgz",
|
||||
"integrity": "sha512-aEADYwRRZjJyMnKN7llGIlircxTCofm3dtV5pmY6ob18MSIuipHpA2yZWkPlycwu5HJcx/pADS3zssd8eY7/6A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.3.3",
|
||||
"jsesc": "^2.5.1",
|
||||
"lodash": "^4.17.11",
|
||||
"source-map": "^0.5.0",
|
||||
"trim-right": "^1.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/types": {
|
||||
"version": "7.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.3.tgz",
|
||||
"integrity": "sha512-2tACZ80Wg09UnPg5uGAOUvvInaqLk3l/IAhQzlxLQOIXacr6bMsra5SH6AWw/hIDRCSbCdHP2KzSOD+cT7TzMQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"esutils": "^2.0.2",
|
||||
"lodash": "^4.17.11",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@babel/helper-replace-supers": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.2.3.tgz",
|
||||
"integrity": "sha512-GyieIznGUfPXPWu0yLS6U55Mz67AZD9cUk0BfirOWlPrXlBcan9Gz+vHGz+cPfuoweZSnPzPIm67VtQM0OWZbA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-member-expression-to-functions": "^7.0.0",
|
||||
"@babel/helper-optimise-call-expression": "^7.0.0",
|
||||
"@babel/traverse": "^7.2.3",
|
||||
"@babel/types": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.3.3.tgz",
|
||||
"integrity": "sha512-xsH1CJoln2r74hR+y7cg2B5JCPaTh+Hd+EbBRk9nWGSNspuo6krjhX0Om6RnRQuIvFq8wVXCLKH3kwKDYhanSg==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/traverse": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.2.3.tgz",
|
||||
"integrity": "sha512-Z31oUD/fJvEWVR0lNZtfgvVt512ForCTNKYcJBGbPb1QZfve4WGH8Wsy7+Mev33/45fhP/hwQtvgusNdcCMgSw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.0.0",
|
||||
"@babel/generator": "^7.2.2",
|
||||
"@babel/helper-function-name": "^7.1.0",
|
||||
"@babel/helper-split-export-declaration": "^7.0.0",
|
||||
"@babel/parser": "^7.2.3",
|
||||
"@babel/types": "^7.2.2",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0",
|
||||
"lodash": "^4.17.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/types": {
|
||||
"version": "7.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.3.tgz",
|
||||
"integrity": "sha512-2tACZ80Wg09UnPg5uGAOUvvInaqLk3l/IAhQzlxLQOIXacr6bMsra5SH6AWw/hIDRCSbCdHP2KzSOD+cT7TzMQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"esutils": "^2.0.2",
|
||||
"lodash": "^4.17.11",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
|
||||
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@babel/helper-define-map": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.1.0.tgz",
|
||||
@@ -302,6 +400,16 @@
|
||||
"@babel/plugin-syntax-async-generators": "^7.2.0"
|
||||
}
|
||||
},
|
||||
"@babel/plugin-proposal-class-properties": {
|
||||
"version": "7.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.3.3.tgz",
|
||||
"integrity": "sha512-XO9eeU1/UwGPM8L+TjnQCykuVcXqaO5J1bkRPIygqZ/A2L1xVMJ9aZXrY31c0U4H2/LHKL4lbFQLsxktSrc/Ng==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-create-class-features-plugin": "^7.3.0",
|
||||
"@babel/helper-plugin-utils": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"@babel/plugin-proposal-json-strings": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
},
|
||||
"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",
|
||||
|
||||
166
src/data-table/data-cell.jsx
Normal file
166
src/data-table/data-cell.jsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ApplyPreMask } from '../masking';
|
||||
import { addSeparators } from '../utilities';
|
||||
|
||||
function formatMeasurementValue (measurement, styling) {
|
||||
// TODO: measurement.name is a horrible propertyname, it's actually the column header
|
||||
const isColumnPercentageBased = measurement.parents.measurement.header.substring(0, 1) === '%';
|
||||
let formattedMeasurementValue = '';
|
||||
if (isColumnPercentageBased) {
|
||||
if (isNaN(measurement.value)) {
|
||||
formattedMeasurementValue = styling.symbolForNulls;
|
||||
} else {
|
||||
formattedMeasurementValue = ApplyPreMask('0,00%', measurement.value);
|
||||
}
|
||||
} else {
|
||||
let magnitudeDivider;
|
||||
switch (measurement.magnitude.toLowerCase()) {
|
||||
case 'k':
|
||||
magnitudeDivider = 1000;
|
||||
break;
|
||||
case 'm':
|
||||
magnitudeDivider = 1000000;
|
||||
break;
|
||||
default:
|
||||
magnitudeDivider = 1;
|
||||
}
|
||||
const formattingStringWithoutMagnitude = measurement.format.replace(/k|K|m|M/gi, '');
|
||||
if (isNaN(measurement.value)) {
|
||||
formattedMeasurementValue = styling.symbolForNulls;
|
||||
} else {
|
||||
let preFormatValue = measurement.value;
|
||||
if (isColumnPercentageBased) {
|
||||
preFormatValue *= 100;
|
||||
}
|
||||
switch (formattingStringWithoutMagnitude) {
|
||||
case '#.##0':
|
||||
formattedMeasurementValue = addSeparators((preFormatValue / magnitudeDivider), '.', ',', 0);
|
||||
break;
|
||||
case '#,##0':
|
||||
formattedMeasurementValue = addSeparators((preFormatValue / magnitudeDivider), ',', '.', 0);
|
||||
break;
|
||||
default:
|
||||
formattedMeasurementValue = ApplyPreMask(
|
||||
formattingStringWithoutMagnitude,
|
||||
(preFormatValue / magnitudeDivider)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return formattedMeasurementValue;
|
||||
}
|
||||
|
||||
function getSemaphoreColors (measurement, semaphoreColors) {
|
||||
if (measurement < semaphoreColors.status.critical) {
|
||||
return semaphoreColors.statusColors.critical;
|
||||
}
|
||||
if (measurement < semaphoreColors.status.medium) {
|
||||
return semaphoreColors.statusColors.medium;
|
||||
}
|
||||
return semaphoreColors.statusColors.normal;
|
||||
}
|
||||
|
||||
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;
|
||||
const isColumnPercentageBased = measurement.name.substring(0, 1) === '%';
|
||||
let formattedMeasurementValue = formatMeasurementValue(measurement, styling);
|
||||
if (styleBuilder.hasComments()) {
|
||||
formattedMeasurementValue = '.';
|
||||
}
|
||||
let textAlignment = 'Right';
|
||||
const textAlignmentProp = styling.options.textAlignment;
|
||||
if (textAlignmentProp) {
|
||||
textAlignment = textAlignmentProp;
|
||||
}
|
||||
|
||||
let cellStyle = {
|
||||
fontFamily: styling.options.fontFamily,
|
||||
...styleBuilder.getStyle(),
|
||||
paddingLeft: '4px',
|
||||
textAlign: textAlignment
|
||||
|
||||
};
|
||||
const { semaphoreColors } = styling;
|
||||
const isValidSemaphoreValue = !styleBuilder.hasComments() && !isNaN(measurement.value);
|
||||
const shouldHaveSemaphoreColors = semaphoreColors.fieldsToApplyTo.applyToAll || semaphoreColors.fieldsToApplyTo.specificFields.indexOf(measurement.parents.dimension1.header) !== -1;
|
||||
if (isValidSemaphoreValue && shouldHaveSemaphoreColors) {
|
||||
const { backgroundColor, color } = getSemaphoreColors(measurement, semaphoreColors);
|
||||
cellStyle = {
|
||||
backgroundColor,
|
||||
color,
|
||||
fontFamily: styling.options.fontFamily,
|
||||
fontSize: styleBuilder.getStyle().fontSize,
|
||||
paddingLeft: '4px',
|
||||
textAlign: textAlignment
|
||||
};
|
||||
}
|
||||
|
||||
let cellClass = 'grid-cells';
|
||||
const shouldUseSmallCells = isColumnPercentageBased && data.headers.measurements.length > 1;
|
||||
if (shouldUseSmallCells) {
|
||||
cellClass = 'grid-cells-small';
|
||||
}
|
||||
|
||||
return (
|
||||
<td
|
||||
className={`${cellClass}${general.cellSuffix}`}
|
||||
onClick={this.handleSelect}
|
||||
style={cellStyle}
|
||||
>
|
||||
{formattedMeasurementValue}
|
||||
</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: PropTypes.func.isRequired
|
||||
}).isRequired
|
||||
}).isRequired,
|
||||
styleBuilder: PropTypes.shape({
|
||||
hasComments: PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
styling: PropTypes.shape({
|
||||
symbolForNulls: PropTypes.any.isRequired
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
export default DataCell;
|
||||
28
src/data-table/header-padding.jsx
Normal file
28
src/data-table/header-padding.jsx
Normal 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;
|
||||
115
src/data-table/index.jsx
Normal file
115
src/data-table/index.jsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import StyleBuilder from '../style-builder';
|
||||
import DataCell from './data-cell.jsx';
|
||||
import HeaderPadding from './header-padding.jsx';
|
||||
import RowHeader from './row-header.jsx';
|
||||
import { injectSeparators } from '../utilities';
|
||||
|
||||
const DataTable = ({ data, general, qlik, renderData, styling }) => {
|
||||
const {
|
||||
headers: {
|
||||
dimension1,
|
||||
measurements
|
||||
},
|
||||
matrix
|
||||
} = data;
|
||||
|
||||
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}>
|
||||
<RowHeader
|
||||
entry={dimensionEntry}
|
||||
qlik={qlik}
|
||||
rowStyle={rowStyle}
|
||||
styleBuilder={styleBuilder}
|
||||
styling={styling}
|
||||
/>
|
||||
{renderData && injectSeparators(
|
||||
matrix[dimensionIndex],
|
||||
styling.useSeparatorColumns,
|
||||
{ atEvery: measurements.length }
|
||||
).map((measurementData, index) => {
|
||||
if (measurementData.isSeparator) {
|
||||
const separatorStyle = {
|
||||
color: 'white',
|
||||
fontFamily: styling.options.fontFamily,
|
||||
fontSize: `${12 + styling.options.fontSizeAdjustment}px`
|
||||
};
|
||||
|
||||
return (
|
||||
<td
|
||||
className="empty"
|
||||
key={`${dimensionEntry.displayValue}-${index}-separator`}
|
||||
style={separatorStyle}
|
||||
>
|
||||
*
|
||||
</td>
|
||||
);
|
||||
}
|
||||
const { dimension1: dimension1Info, dimension2, measurement } = measurementData.parents;
|
||||
const id = `${dimension1Info.elementNumber}-${dimension2 && dimension2.elementNumber}-${measurement.header}`;
|
||||
return (
|
||||
<DataCell
|
||||
data={data}
|
||||
general={general}
|
||||
key={`${dimensionEntry.displayValue}-${id}`}
|
||||
measurement={measurementData}
|
||||
qlik={qlik}
|
||||
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;
|
||||
49
src/data-table/row-header.jsx
Normal file
49
src/data-table/row-header.jsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import HeaderPadding from './header-padding.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 } = this.props;
|
||||
return (
|
||||
<td
|
||||
className="fdim-cells"
|
||||
onClick={this.handleSelect}
|
||||
style={rowStyle}
|
||||
>
|
||||
<HeaderPadding
|
||||
styleBuilder={styleBuilder}
|
||||
styling={styling}
|
||||
/>
|
||||
{entry.displayValue}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RowHeader.propTypes = {
|
||||
entry: PropTypes.shape({
|
||||
displayValue: PropTypes.string.isRequired
|
||||
}).isRequired,
|
||||
qlik: PropTypes.shape({
|
||||
backendApi: PropTypes.shape({
|
||||
selectValues: PropTypes.func.isRequired
|
||||
}).isRequired
|
||||
}).isRequired,
|
||||
rowStyle: PropTypes.shape({}).isRequired,
|
||||
styleBuilder: PropTypes.shape({}).isRequired,
|
||||
styling: PropTypes.shape({}).isRequired
|
||||
};
|
||||
|
||||
export default RowHeader;
|
||||
@@ -139,6 +139,10 @@ const formatted = {
|
||||
component: 'dropdown',
|
||||
label: 'FontFamily',
|
||||
options: [
|
||||
{
|
||||
value: 'QlikView Sans',
|
||||
label: 'QlikView Sans'
|
||||
},
|
||||
{
|
||||
value: 'Arial',
|
||||
label: 'Arial'
|
||||
@@ -164,7 +168,7 @@ const formatted = {
|
||||
label: 'Verdana'
|
||||
}
|
||||
],
|
||||
defaultValue: 'Calibri'
|
||||
defaultValue: 'QlikView Sans'
|
||||
},
|
||||
DataFontSize: {
|
||||
ref: 'lettersize',
|
||||
@@ -181,7 +185,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',
|
||||
|
||||
@@ -27,7 +27,7 @@ const header = {
|
||||
ref: 'HeaderColorSchema',
|
||||
type: 'string',
|
||||
component: 'dropdown',
|
||||
label: 'BackGround Header Color',
|
||||
label: 'Background Header Color',
|
||||
options: [
|
||||
{
|
||||
value: 'Clean',
|
||||
|
||||
@@ -9,15 +9,16 @@ import pijamaColorLibrary from './pijama-color-library';
|
||||
const definition = {
|
||||
component: 'accordion',
|
||||
items: {
|
||||
dimensions: {
|
||||
max: 2,
|
||||
min: 1,
|
||||
uses: 'dimensions'
|
||||
},
|
||||
measures: {
|
||||
max: 9,
|
||||
min: 1,
|
||||
uses: 'measures'
|
||||
data: {
|
||||
items: {
|
||||
dimensions: {
|
||||
disabledRef: ''
|
||||
},
|
||||
measures: {
|
||||
disabledRef: ''
|
||||
}
|
||||
},
|
||||
uses: 'data'
|
||||
},
|
||||
settings: {
|
||||
items: {
|
||||
|
||||
@@ -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;
|
||||
@@ -1,73 +1,86 @@
|
||||
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 removeAllTooltips (node) {
|
||||
const tooltips = node.querySelectorAll('.tooltip');
|
||||
[].forEach.call(tooltips, tooltip => {
|
||||
if (tooltip.parentNode) {
|
||||
tooltip.parentNode.removeChild(tooltip);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function buildTableHTML (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);
|
||||
|
||||
removeAllTooltips(dataTableClone);
|
||||
|
||||
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 : ''}
|
||||
${footnoteHTML.length > 0 ? footnoteHTML : ''}
|
||||
${dataTableClone.outerHTML}
|
||||
</body>
|
||||
</html>
|
||||
`.split('>.<')
|
||||
.join('><')
|
||||
.split('>*<')
|
||||
.join('><');
|
||||
|
||||
return tableHTML;
|
||||
}
|
||||
|
||||
function downloadXLS (html) {
|
||||
const filename = 'analysis.xls';
|
||||
// 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.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
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) {
|
||||
// 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);
|
||||
downloadXLS(table);
|
||||
}
|
||||
|
||||
@@ -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 { excelExport, general } = this.props;
|
||||
const { title, subtitle, footnote } = general;
|
||||
if (excelExport) {
|
||||
exportXLS(title, subtitle, footnote);
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { excelExport } = this.props;
|
||||
return excelExport === true && (
|
||||
<input
|
||||
className="icon-xls"
|
||||
onClick={this.handleExport}
|
||||
src="/Extensions/qlik-smart-pivot/Excel.png"
|
||||
type="image"
|
||||
/>
|
||||
@@ -20,7 +34,8 @@ ExportButton.defaultProps = {
|
||||
};
|
||||
|
||||
ExportButton.propTypes = {
|
||||
excelExport: PropTypes.bool
|
||||
excelExport: PropTypes.bool,
|
||||
general: PropTypes.shape({}).isRequired
|
||||
};
|
||||
|
||||
export default ExportButton;
|
||||
|
||||
@@ -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;
|
||||
63
src/headers-table/column-header.jsx
Normal file
63
src/headers-table/column-header.jsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
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 } = this.props;
|
||||
const style = {
|
||||
...baseCSS,
|
||||
fontSize: `${14 + styling.headerOptions.fontSizeAdjustment} px`,
|
||||
height: '45px',
|
||||
verticalAlign: 'middle'
|
||||
};
|
||||
|
||||
return (
|
||||
<th
|
||||
className={`grid-cells2${cellSuffix}`}
|
||||
colSpan={colSpan}
|
||||
onClick={this.handleSelect}
|
||||
style={style}
|
||||
>
|
||||
{entry.displayValue}
|
||||
</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: PropTypes.func.isRequired
|
||||
}).isRequired
|
||||
}).isRequired,
|
||||
styling: PropTypes.shape({
|
||||
headerOptions: PropTypes.shape({
|
||||
fontSizeAdjustment: PropTypes.number.isRequired
|
||||
}).isRequired
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
export default ColumnHeader;
|
||||
44
src/headers-table/export-column-header.jsx
Normal file
44
src/headers-table/export-column-header.jsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ExportButton from '../export-button.jsx';
|
||||
|
||||
const ExportColumnHeader = ({ baseCSS, general, title, allowExcelExport, hasSecondDimension, styling }) => {
|
||||
const rowSpan = hasSecondDimension ? 2 : 1;
|
||||
const style = {
|
||||
...baseCSS,
|
||||
cursor: 'default',
|
||||
fontSize: `${16 + styling.headerOptions.fontSizeAdjustment} px`,
|
||||
height: '80px',
|
||||
verticalAlign: 'middle',
|
||||
width: '230px'
|
||||
};
|
||||
|
||||
return (
|
||||
<th
|
||||
className="fdim-cells"
|
||||
rowSpan={rowSpan}
|
||||
style={style}
|
||||
>
|
||||
<ExportButton
|
||||
excelExport={allowExcelExport}
|
||||
general={general}
|
||||
/>
|
||||
{title}
|
||||
</th>
|
||||
);
|
||||
};
|
||||
|
||||
ExportColumnHeader.propTypes = {
|
||||
allowExcelExport: PropTypes.bool.isRequired,
|
||||
baseCSS: PropTypes.shape({}).isRequired,
|
||||
general: PropTypes.shape({}).isRequired,
|
||||
hasSecondDimension: PropTypes.bool.isRequired,
|
||||
styling: PropTypes.shape({
|
||||
headerOptions: PropTypes.shape({
|
||||
fontSizeAdjustment: PropTypes.number.isRequired
|
||||
}).isRequired
|
||||
}).isRequired,
|
||||
title: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default ExportColumnHeader;
|
||||
138
src/headers-table/index.jsx
Normal file
138
src/headers-table/index.jsx
Normal file
@@ -0,0 +1,138 @@
|
||||
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 }) => {
|
||||
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>
|
||||
<ExportColumnHeader
|
||||
allowExcelExport={general.allowExcelExport}
|
||||
baseCSS={baseCSS}
|
||||
general={general}
|
||||
hasSecondDimension={hasSecondDimension}
|
||||
styling={styling}
|
||||
title={dimension1[0].name}
|
||||
/>
|
||||
{!hasSecondDimension && measurements.map(measurementEntry => (
|
||||
<MeasurementColumnHeader
|
||||
baseCSS={baseCSS}
|
||||
general={general}
|
||||
hasSecondDimension={hasSecondDimension}
|
||||
key={`${measurementEntry.displayValue}-${measurementEntry.name}`}
|
||||
measurement={measurementEntry}
|
||||
styling={styling}
|
||||
/>
|
||||
))}
|
||||
{hasSecondDimension && injectSeparators(dimension2, styling.useSeparatorColumns).map((entry, index) => {
|
||||
if (entry.isSeparator) {
|
||||
const separatorStyle = {
|
||||
color: 'white',
|
||||
fontFamily: styling.options.fontFamily,
|
||||
fontSize: `${13 + styling.headerOptions.fontSizeAdjustment}px`
|
||||
};
|
||||
|
||||
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>
|
||||
{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: PropTypes.func.isRequired
|
||||
}).isRequired
|
||||
}).isRequired,
|
||||
styling: PropTypes.shape({
|
||||
headerOptions: PropTypes.shape({}),
|
||||
options: PropTypes.shape({})
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
export default HeadersTable;
|
||||
77
src/headers-table/measurement-column-header.jsx
Normal file
77
src/headers-table/measurement-column-header.jsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const MeasurementColumnHeader = ({ baseCSS, general, hasSecondDimension, measurement, styling }) => {
|
||||
const title = `${measurement.name} ${measurement.magnitudeLabelSuffix}`;
|
||||
const { fontSizeAdjustment } = styling.headerOptions;
|
||||
if (hasSecondDimension) {
|
||||
const isPercentageFormat = measurement.format.substring(measurement.format.length - 1) === '%';
|
||||
let baseFontSize = 14;
|
||||
let cellClass = 'grid-cells2';
|
||||
if (isPercentageFormat) {
|
||||
baseFontSize = 13;
|
||||
cellClass = 'grid-cells2-small';
|
||||
}
|
||||
const cellStyle = {
|
||||
...baseCSS,
|
||||
cursor: 'default',
|
||||
fontSize: `${baseFontSize + fontSizeAdjustment} px`,
|
||||
height: '25px',
|
||||
verticalAlign: 'middle'
|
||||
};
|
||||
return (
|
||||
<th
|
||||
className={`${cellClass}${general.cellSuffix}`}
|
||||
style={cellStyle}
|
||||
>
|
||||
<span className="wrapclass25">
|
||||
{title}
|
||||
</span>
|
||||
</th>
|
||||
);
|
||||
}
|
||||
const isLong = (title.length > 11 && fontSizeAdjustment === 0) || (title.length > 12 && fontSizeAdjustment === -2);
|
||||
const suffixWrap = isLong ? '70' : 'empty';
|
||||
const style = {
|
||||
...baseCSS,
|
||||
cursor: 'default',
|
||||
fontSize: `${15 + fontSizeAdjustment} px`,
|
||||
height: '70px',
|
||||
verticalAlign: 'middle'
|
||||
};
|
||||
return (
|
||||
<th
|
||||
className={`grid-cells2 ${general.cellSuffix}`}
|
||||
style={style}
|
||||
>
|
||||
<span
|
||||
className={`wrapclass${suffixWrap}`}
|
||||
style={{ fontFamily: styling.headerOptions.fontFamily }}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
</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;
|
||||
19
src/index.js
19
src/index.js
@@ -1,4 +1,4 @@
|
||||
import paint from './paint';
|
||||
import paint from './paint.jsx';
|
||||
import definition from './definition';
|
||||
import './main.less';
|
||||
|
||||
@@ -12,6 +12,18 @@ export default {
|
||||
'$timeout',
|
||||
function () { }
|
||||
],
|
||||
data: {
|
||||
dimensions: {
|
||||
max: 2,
|
||||
min: 1,
|
||||
uses: 'dimensions'
|
||||
},
|
||||
measures: {
|
||||
max: 9,
|
||||
min: 1,
|
||||
uses: 'measures'
|
||||
}
|
||||
},
|
||||
definition,
|
||||
initialProperties: {
|
||||
qHyperCubeDef: {
|
||||
@@ -25,6 +37,11 @@ export default {
|
||||
qMeasures: []
|
||||
}
|
||||
},
|
||||
support: {
|
||||
export: true,
|
||||
exportData: true,
|
||||
snapshot: true
|
||||
},
|
||||
paint ($element, layout) {
|
||||
try {
|
||||
paint($element, layout, this);
|
||||
|
||||
346
src/initialize-transformed.js
Normal file
346
src/initialize-transformed.js
Normal file
@@ -0,0 +1,346 @@
|
||||
import jQuery from 'jquery';
|
||||
import { distinctArray } from './utilities';
|
||||
|
||||
// TODO: rename colors
|
||||
function initializeColors ({ layout }) {
|
||||
return {
|
||||
vColLibBlue: layout.collibblue,
|
||||
vColLibBlueP: layout.collibbluep,
|
||||
vColLibClean: layout.collibclean,
|
||||
vColLibCleanP: layout.collibcleanp,
|
||||
vColLibCustom: layout.collibcustom,
|
||||
vColLibCustomP: layout.collibcustomp,
|
||||
vColLibDark: layout.collibdark,
|
||||
vColLibDarkP: layout.collibdarkp,
|
||||
vColLibGreen: layout.collibgreen,
|
||||
vColLibGreenP: layout.collibgreenp,
|
||||
vColLibNight: layout.collibnight,
|
||||
vColLibNightP: layout.collibnightp,
|
||||
vColLibOrange: layout.colliborange,
|
||||
vColLibOrangeP: layout.colliborangep,
|
||||
vColLibRed: layout.collibred,
|
||||
vColLibRedP: layout.collibredp,
|
||||
vColLibSoft: layout.collibsoft,
|
||||
vColLibSoftP: layout.collibsoftp,
|
||||
vColLibViolete: layout.collibviolete,
|
||||
vColLibVioleteP: layout.collibvioletep
|
||||
};
|
||||
}
|
||||
|
||||
function getAlignment (option) {
|
||||
const alignmentOptions = {
|
||||
1: 'left',
|
||||
2: 'center',
|
||||
3: 'right'
|
||||
};
|
||||
|
||||
return alignmentOptions[option] || 'left';
|
||||
}
|
||||
|
||||
function getFontSizeAdjustment (option) {
|
||||
const fontSizeAdjustmentOptions = {
|
||||
1: -1,
|
||||
2: 1,
|
||||
3: 2
|
||||
};
|
||||
|
||||
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,
|
||||
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
|
||||
}
|
||||
},
|
||||
value: cell.qNum
|
||||
};
|
||||
|
||||
if (dimension2Information) {
|
||||
matrixCell.parents.dimension2 = {
|
||||
elementNumber: dimension2Information.qElemNumber
|
||||
};
|
||||
}
|
||||
|
||||
return matrixCell;
|
||||
}
|
||||
|
||||
let lastRow = 0;
|
||||
function generateDataSet (component, dimensionsInformation, measurementsInformation) {
|
||||
const dimension1 = [];
|
||||
const dimension2 = [];
|
||||
const measurements = generateMeasurements(measurementsInformation);
|
||||
let matrix = [];
|
||||
|
||||
let previousDim1Entry;
|
||||
const hasSecondDimension = dimensionsInformation.length > 1;
|
||||
component.backendApi.eachDataRow((rowIndex, row) => {
|
||||
lastRow += 1;
|
||||
const dimension1Entry = generateDimensionEntry(dimensionsInformation[0], row[0]);
|
||||
dimension1.push(dimension1Entry);
|
||||
let dimension2Entry;
|
||||
let firstDataCell = 1;
|
||||
if (hasSecondDimension) {
|
||||
dimension2Entry = generateDimensionEntry(dimensionsInformation[1], row[1]);
|
||||
dimension2.push(dimension2Entry);
|
||||
firstDataCell = 2;
|
||||
}
|
||||
const matrixRow = row
|
||||
.slice(firstDataCell, row.length)
|
||||
.map((cell, cellIndex) => {
|
||||
const measurementInformation = measurements[cellIndex];
|
||||
const dimension1Information = row[0]; // eslint-disable-line prefer-destructuring
|
||||
const dimension2Information = hasSecondDimension ? row[1] : null;
|
||||
const generatedCell = generateMatrixCell({
|
||||
cell,
|
||||
dimension1Information,
|
||||
dimension2Information,
|
||||
measurementInformation
|
||||
});
|
||||
|
||||
return generatedCell;
|
||||
});
|
||||
|
||||
if (hasSecondDimension) {
|
||||
const currentDim1Entry = row[0].qText;
|
||||
const isSameDimension1AsPrevious = currentDim1Entry === previousDim1Entry;
|
||||
if (isSameDimension1AsPrevious) {
|
||||
const updatedRow = matrix[matrix.length - 1].concat(matrixRow);
|
||||
|
||||
matrix = [
|
||||
...matrix.slice(0, matrix.length - 1),
|
||||
updatedRow
|
||||
];
|
||||
} else {
|
||||
matrix[matrix.length] = matrixRow;
|
||||
}
|
||||
previousDim1Entry = currentDim1Entry;
|
||||
} else {
|
||||
matrix[matrix.length] = matrixRow;
|
||||
}
|
||||
});
|
||||
|
||||
// filter header dimensions to only have distinct values
|
||||
|
||||
return {
|
||||
dimension1: distinctArray(dimension1),
|
||||
dimension2: distinctArray(dimension2),
|
||||
matrix,
|
||||
measurements
|
||||
};
|
||||
}
|
||||
|
||||
async function initializeTransformed ({ $element, layout, component }) {
|
||||
const colors = initializeColors({ layout });
|
||||
const dimensionsInformation = component.backendApi.getDimensionInfos();
|
||||
const measurementsInformation = component.backendApi.getMeasureInfos();
|
||||
const dimensionCount = layout.qHyperCube.qDimensionInfo.length;
|
||||
const rowCount = component.backendApi.getRowCount();
|
||||
const maxLoops = layout.maxloops;
|
||||
const {
|
||||
dimension1,
|
||||
dimension2,
|
||||
measurements,
|
||||
matrix
|
||||
} = generateDataSet(component, dimensionsInformation, measurementsInformation);
|
||||
|
||||
const customSchemaBasic = [];
|
||||
const customSchemaFull = [];
|
||||
let customHeadersCount = 0;
|
||||
|
||||
function readCustomSchema () {
|
||||
const url = `/Extensions/qlik-smart-pivot/${layout.customfile}`;
|
||||
|
||||
return jQuery.get(url).then(response => {
|
||||
const allTextLines = response.split(/\r\n|\n/);
|
||||
const headers = allTextLines[0].split(';');
|
||||
customHeadersCount = headers.length;
|
||||
for (let lineNumber = 0; lineNumber < allTextLines.length; lineNumber += 1) {
|
||||
customSchemaFull[lineNumber] = new Array(headers.length);
|
||||
const data = allTextLines[lineNumber].split(';');
|
||||
|
||||
if (data.length === headers.length) {
|
||||
for (let headerIndex = 0; headerIndex < headers.length; headerIndex += 1) {
|
||||
customSchemaBasic[lineNumber] = data[0];
|
||||
customSchemaFull[lineNumber][headerIndex] = data[headerIndex];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const hasCustomSchema = (layout.customfilebool && layout.customfile.length > 4);
|
||||
const schemaPromise = hasCustomSchema ? readCustomSchema() : Promise.resolve();
|
||||
await schemaPromise;
|
||||
|
||||
// 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: {
|
||||
colors,
|
||||
customCSV: {
|
||||
basic: customSchemaBasic,
|
||||
count: customHeadersCount,
|
||||
full: customSchemaFull
|
||||
},
|
||||
hasCustomFileStyle: layout.customfilebool,
|
||||
headerOptions: {
|
||||
alignment: getAlignment(layout.HeaderAlign),
|
||||
colorSchema: colors[`vColLib${layout.HeaderColorSchema}`],
|
||||
fontSizeAdjustment: getFontSizeAdjustment(layout.lettersizeheader),
|
||||
textColor: layout.HeaderTextColorSchema
|
||||
},
|
||||
options: {
|
||||
backgroundColor: colors[`vColLib${layout.ColorSchema}`],
|
||||
backgroundColorOdd: colors[`vColLib${layout.ColorSchemaP}`],
|
||||
color: layout.BodyTextColorSchema,
|
||||
fontFamily: layout.FontFamily,
|
||||
fontSizeAdjustment: getFontSizeAdjustment(layout.lettersize),
|
||||
textAlignment: layout.cellTextAlignment
|
||||
},
|
||||
semaphoreColors: {
|
||||
fieldsToApplyTo: {
|
||||
applyToAll: layout.allsemaphores,
|
||||
specificFields: [
|
||||
layout.conceptsemaphore1,
|
||||
layout.conceptsemaphore2,
|
||||
layout.conceptsemaphore3,
|
||||
layout.conceptsemaphore4,
|
||||
layout.conceptsemaphore5,
|
||||
layout.conceptsemaphore6,
|
||||
layout.conceptsemaphore7,
|
||||
layout.conceptsemaphore9,
|
||||
layout.conceptsemaphore10
|
||||
]
|
||||
},
|
||||
status: {
|
||||
critical: layout.metricstatus1,
|
||||
medium: layout.metricstatus2
|
||||
},
|
||||
statusColors: {
|
||||
critical: {
|
||||
backgroundColor: layout.colorstatus1.color,
|
||||
color: layout.colorstatus1text.color
|
||||
},
|
||||
medium: {
|
||||
backgroundColor: layout.colorstatus2.color,
|
||||
color: layout.colorstatus2text.color
|
||||
},
|
||||
normal: {
|
||||
backgroundColor: layout.colorstatus3.color,
|
||||
color: layout.colorstatus3text.color
|
||||
}
|
||||
}
|
||||
},
|
||||
symbolForNulls: layout.symbolfornulls,
|
||||
usePadding: layout.indentbool,
|
||||
useSeparatorColumns: dimensionCount === 1 ? false : layout.separatorcols
|
||||
}
|
||||
};
|
||||
|
||||
if (rowCount > lastRow && rowCount <= (maxLoops * 1000)) {
|
||||
const requestPage = [
|
||||
{
|
||||
qHeight: Math.min(1000, rowCount - lastRow),
|
||||
qLeft: 0,
|
||||
qTop: matrix.length,
|
||||
qWidth: 10 // should be # of columns
|
||||
}
|
||||
];
|
||||
component.backendApi.getData(requestPage).then(() => {
|
||||
component.paint($element, layout);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return transformedProperties;
|
||||
}
|
||||
|
||||
export default initializeTransformed;
|
||||
2
src/linked-scroll/index.js
Normal file
2
src/linked-scroll/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as LinkedScrollWrapper } from './linked-scroll-wrapper.jsx';
|
||||
export { default as LinkedScrollSection } from './linked-scroll-section.jsx';
|
||||
29
src/linked-scroll/linked-scroll-section.jsx
Normal file
29
src/linked-scroll/linked-scroll-section.jsx
Normal 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;
|
||||
82
src/linked-scroll/linked-scroll-wrapper.jsx
Normal file
82
src/linked-scroll/linked-scroll-wrapper.jsx
Normal 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;
|
||||
@@ -1,7 +1,9 @@
|
||||
.qv-object-qlik-smart-pivot {
|
||||
@TableBorder: 1px solid #d3d3d3;
|
||||
@KpiTableWidth: 230px;
|
||||
|
||||
.edit-mode{
|
||||
pointer-events: none;
|
||||
}
|
||||
._cell(@Width: 50px) {
|
||||
min-width: @Width!important;
|
||||
max-width: @Width!important;
|
||||
@@ -10,8 +12,6 @@
|
||||
}
|
||||
|
||||
div.qv-object-content-container {
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
z-index: 110;
|
||||
}
|
||||
|
||||
@@ -149,12 +149,6 @@
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
.header-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/*popups for headers*/
|
||||
.tooltip {
|
||||
position: fixed !important;
|
||||
@@ -166,11 +160,7 @@
|
||||
|
||||
/*end popups*/
|
||||
.row-wrapper {
|
||||
position: absolute;
|
||||
top: 97px;
|
||||
height: calc(~"100% - 97px");
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
padding: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
@@ -186,30 +176,54 @@
|
||||
.kpi-table {
|
||||
width: @KpiTableWidth !important;
|
||||
overflow: hidden !important;
|
||||
display: table;
|
||||
height: 100%;
|
||||
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;
|
||||
}
|
||||
|
||||
.kpi-table .row-wrapper {
|
||||
overflow: hidden;
|
||||
.row-wrapper {
|
||||
height: calc(~"100% - 97px");
|
||||
overflow: scroll;
|
||||
position: absolute;
|
||||
padding: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 272px !important;
|
||||
float: left;
|
||||
display: table;
|
||||
height: 100%;
|
||||
z-index: 90;
|
||||
width: calc(100% - 243px);
|
||||
position: absolute;
|
||||
margin-left: @KpiTableWidth + 13px;
|
||||
-ms-overflow-style: none;
|
||||
|
||||
.header-wrapper {
|
||||
overflow: scroll;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.row-wrapper {
|
||||
height: calc(~"100% - 97px");
|
||||
width: 100%;
|
||||
overflow: scroll;
|
||||
padding: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// hide scrollbars
|
||||
.kpi-table .header-wrapper,
|
||||
.kpi-table .row-wrapper,
|
||||
.data-table .header-wrapper,
|
||||
.data-table .row-wrapper {
|
||||
-ms-overflow-style: none; // IE 10+
|
||||
-moz-overflow: -moz-scrollbars-none; // Firefox
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none; // Safari and Chrome
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
210
src/paint.js
210
src/paint.js
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
83
src/paint.jsx
Normal file
83
src/paint.jsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import $ from 'jquery';
|
||||
import initializeStore from './store';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import HeadersTable from './headers-table/index.jsx';
|
||||
import DataTable from './data-table/index.jsx';
|
||||
import { LinkedScrollWrapper, LinkedScrollSection } from './linked-scroll';
|
||||
|
||||
export default async function paint ($element, layout, component) {
|
||||
const state = await initializeStore({
|
||||
$element,
|
||||
component,
|
||||
layout
|
||||
});
|
||||
const editmodeClass = component.inAnalysisState() ? '' : 'edit-mode';
|
||||
const jsx = (
|
||||
<LinkedScrollWrapper>
|
||||
<div className={`kpi-table ${editmodeClass}`}>
|
||||
<HeadersTable
|
||||
data={state.data}
|
||||
general={state.general}
|
||||
qlik={component}
|
||||
styling={state.styling}
|
||||
/>
|
||||
<LinkedScrollSection linkVertical>
|
||||
<DataTable
|
||||
data={state.data}
|
||||
general={state.general}
|
||||
qlik={component}
|
||||
renderData={false}
|
||||
styling={state.styling}
|
||||
/>
|
||||
</LinkedScrollSection>
|
||||
</div>
|
||||
<div className={`data-table ${editmodeClass}`}>
|
||||
<LinkedScrollSection linkHorizontal>
|
||||
<HeadersTable
|
||||
data={state.data}
|
||||
general={state.general}
|
||||
qlik={component}
|
||||
styling={state.styling}
|
||||
/>
|
||||
</LinkedScrollSection>
|
||||
<LinkedScrollSection
|
||||
linkHorizontal
|
||||
linkVertical
|
||||
>
|
||||
<DataTable
|
||||
data={state.data}
|
||||
general={state.general}
|
||||
qlik={component}
|
||||
styling={state.styling}
|
||||
/>
|
||||
</LinkedScrollSection>
|
||||
</div>
|
||||
</LinkedScrollWrapper>
|
||||
);
|
||||
|
||||
ReactDOM.render(jsx, $element[0]);
|
||||
|
||||
// TODO: fixing tooltips has a seperate issue, make sure to remove this as part of that issue
|
||||
$(`[tid="${layout.qInfo.qId}"] .header-wrapper th`).hover(function () {
|
||||
$(`[tid="${layout.qInfo.qId}"] .tooltip`).delay(500)
|
||||
.show(0);
|
||||
$(`[tid="${layout.qInfo.qId}"] .header-wrapper th`).children(`[tid="${layout.qInfo.qId}"] .tooltip`)
|
||||
.remove();
|
||||
|
||||
const element = $(this);
|
||||
const offset = element.offset();
|
||||
const toolTip = $('<div class="tooltip"></div>');
|
||||
|
||||
toolTip.css({
|
||||
left: offset.left,
|
||||
top: offset.top
|
||||
});
|
||||
|
||||
toolTip.text(element.text());
|
||||
$(`[tid="${layout.qInfo.qId}"] .header-wrapper th`).append(toolTip);
|
||||
}, () => {
|
||||
$(`[tid="${layout.qInfo.qId}"] .tooltip`).delay(0)
|
||||
.hide(0);
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
300
src/store.js
300
src/store.js
@@ -1,299 +1,13 @@
|
||||
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 }) {
|
||||
const transformedProperties = await initializeTransformed({
|
||||
$element,
|
||||
component,
|
||||
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;
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
function StyleBuilder (state) {
|
||||
function StyleBuilder (styling) {
|
||||
const {
|
||||
CustomArray,
|
||||
CustomArrayBasic,
|
||||
vNumCustomHeaders,
|
||||
vColorSchema,
|
||||
vColorText,
|
||||
vColorSchemaP,
|
||||
vLetterSize,
|
||||
colors
|
||||
} = state;
|
||||
colors,
|
||||
customCSV,
|
||||
options
|
||||
} = styling;
|
||||
let style = {
|
||||
fontSize: `${14 + vLetterSize}px`
|
||||
fontSize: `${14 + options.fontSizeAdjustment}px`
|
||||
};
|
||||
let hasComments = false;
|
||||
let commentColor;
|
||||
@@ -18,9 +13,9 @@ function StyleBuilder (state) {
|
||||
|
||||
function applyStandardAttributes (rowNumber) {
|
||||
const isEven = rowNumber % 2 === 0;
|
||||
style.backgroundColor = isEven ? vColorSchema : vColorSchemaP;
|
||||
style.color = vColorText;
|
||||
style.fontSize = `${14 + vLetterSize}px`;
|
||||
style.backgroundColor = isEven ? options.backgroundColor : options.backgroundColorOdd;
|
||||
style.color = options.color;
|
||||
style.fontSize = `${13 + options.fontSizeAdjustment}px`;
|
||||
}
|
||||
|
||||
function applyColor (color) {
|
||||
@@ -28,6 +23,7 @@ function StyleBuilder (state) {
|
||||
commentColor = color;
|
||||
}
|
||||
|
||||
/* eslint-disable sort-keys*/
|
||||
const properties = {
|
||||
'<comment>': () => { hasComments = true; },
|
||||
// text
|
||||
@@ -46,17 +42,18 @@ function StyleBuilder (state) {
|
||||
// font color TODO: this is a color just like the others, but it applies to text instead.. any way to make it less weird?
|
||||
'<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 +81,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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,13 @@ export function onlyUnique (value, index, self) {
|
||||
return self.indexOf(value) === index;
|
||||
}
|
||||
|
||||
export function distinctArray (array) {
|
||||
return array
|
||||
.map(entry => JSON.stringify(entry))
|
||||
.filter(onlyUnique)
|
||||
.map(entry => JSON.parse(entry));
|
||||
}
|
||||
|
||||
export function addSeparators (nStr, thousandsSep, decimalSep, numDecimals) {
|
||||
let x1;
|
||||
nStr = nStr.toFixed(numDecimals);
|
||||
@@ -21,3 +28,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;
|
||||
}, []);
|
||||
}
|
||||
|
||||
@@ -6,23 +6,16 @@ console.log('Webpack mode:', settings.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',
|
||||
commonjs: 'jquery',
|
||||
commonjs2: 'jquery',
|
||||
root: '_'
|
||||
},
|
||||
}
|
||||
},
|
||||
mode: settings.mode,
|
||||
// TODO: breaks core-js for some reason
|
||||
// resolve: {
|
||||
// extensions: ['js', 'jsx']
|
||||
@@ -31,20 +24,23 @@ const config = {
|
||||
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,21 +50,30 @@ const config = {
|
||||
},
|
||||
{
|
||||
test: /.less$/,
|
||||
use: ['style-loader', 'css-loader', 'less-loader']
|
||||
use: [
|
||||
'style-loader',
|
||||
'css-loader',
|
||||
'less-loader'
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
output: {
|
||||
filename: `${settings.name}.js`,
|
||||
libraryTarget: 'amd',
|
||||
path: settings.buildDestination
|
||||
},
|
||||
plugins: [
|
||||
new CopyWebpackPlugin([
|
||||
'assets/' + settings.name + '.qext',
|
||||
'assets/' + settings.name + '.png',
|
||||
`assets/${settings.name}.qext`,
|
||||
`assets/${settings.name}.png`,
|
||||
'assets/wbfolder.wbl',
|
||||
'resources/Excel.png',
|
||||
|
||||
// TODO: remove entries below this line
|
||||
'resources/Accounts.csv',
|
||||
'resources/Accounts2.csv',
|
||||
'resources/QlikLook.csv',
|
||||
'resources/Excel.png',
|
||||
'resources/QlikLook.csv'
|
||||
], {}),
|
||||
new StyleLintPlugin()
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user