Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79339a8963 | ||
|
|
a6faeeb656 | ||
|
|
c22b7f5c6b | ||
|
|
80f97602e4 | ||
|
|
f745656b4c | ||
|
|
f7e780b92e | ||
|
|
d7a76c7db9 | ||
|
|
16c380e1c6 | ||
|
|
072a3b80c4 | ||
|
|
5572b7ce67 | ||
|
|
71cf92c217 | ||
|
|
e26d5fded8 | ||
|
|
20282b0b99 | ||
|
|
5f18321ccf | ||
|
|
d14f5951ac | ||
|
|
5872ee7b58 | ||
|
|
1436463f59 | ||
|
|
729a31920d | ||
|
|
6d305b21b2 | ||
|
|
0b210e0d35 | ||
|
|
c8afb83130 | ||
|
|
49981f6ae3 |
44
README.md
44
README.md
@@ -1,52 +1,22 @@
|
||||
# P&L Smart Pivot, a Qlik Sense Extension for Financial reporting
|
||||
[](https://circleci.com/gh/qlik-oss/PLSmartPivot)
|
||||
This extension is part of the extension bundles for Qlik Sense. The repository is maintained and moderated by Qlik RD.
|
||||
|
||||
This extension is useful to create reports where the look&feel is rellevantand and pivot a second dimension is needed. Based on P&L Smart.
|
||||
|
||||
It's specifically focused on financial reports, trying to solve some common needs of this area:
|
||||
- smart export to excel
|
||||
- easy creation of reports
|
||||
- custom corporate reporting (bold, italic, background color, letter size, headers,...)
|
||||
- selections inside the reports
|
||||
- custom external templates
|
||||
- analytical reports
|
||||
|
||||
|
||||
# Manual
|
||||
You'll find a manual [Qlik Sense P&LSmart Pivot Extension Manual.pdf](resources/Qlik Sense P&LSmart Pivot Extension Manual.pdf) and one app example [P&LSmartPivot_demo.qvf](resources/P&LSmartPivot_demo.qvf).
|
||||
|
||||
|
||||
# Installation
|
||||
|
||||
1. Download the extension zip, `qlik-smart-pivot_<version>.zip`, from the latest release(https://github.com/qlik-oss/PLSmartPivot/releases/latest)
|
||||
2. Install the extension:
|
||||
|
||||
a. **Qlik Sense Desktop**: unzip to a directory under [My Documents]/Qlik/Sense/Extensions.
|
||||
|
||||
b. **Qlik Sense Server**: import the zip file in the QMC.
|
||||
Feel free to fork and suggest pull requests for improvements and bug fixes. Changes will be moderated and reviewed before inclusion in future bundle versions. Please note that emphasis is on backward compatibility, i.e. breaking changes will most likely not be approved.
|
||||
|
||||
Usage documentation for the extension is available at https://help.qlik.com.
|
||||
|
||||
# Developing the extension
|
||||
|
||||
If you want to do code changes to the extension follow these simple steps to get going.
|
||||
|
||||
1. Get Qlik Sense Desktop
|
||||
1. Create a new app and add the extension to a sheet.
|
||||
1. Create a new app and add P&L pivot to a sheet.
|
||||
2. Clone the repository
|
||||
3. Run `npm install`
|
||||
4. Set the environment variable `BUILD_PATH` to your extensions directory. It will be something like `C:/Users/<user>/Documents/Qlik/Sense/Extensions/<extension_name>`.
|
||||
5. You now have two options. Either run the watch task or the build task. They are explained below. Both of them default to development mode but can be run in production by setting `NODE_ENV=production` before running the npm task.
|
||||
|
||||
a. **Watch**: `npm run watch`. This will start a watcher which will rebuild the extension and output all needed files to the `buildFolder` for each code change you make. See your changes directly in your Qlik Sense app.
|
||||
|
||||
b. **Build**: `npm run build`. If you want to build the extension package. The output zip-file can be found in the `buildFolder`.
|
||||
|
||||
4. Run `npm run build` - to build a dev-version to the /dist folder.
|
||||
5. Move the content of the /dist folder to the extension directory. Usually in `C:/Users/<user>/Documents/Qlik/Sense/Extensions/qlik-smart-pivot`.
|
||||
|
||||
# Original authors
|
||||
|
||||
[github.com/iviasensio](https://github.com/iviasensio)
|
||||
|
||||
|
||||
# License
|
||||
|
||||
Released under the [MIT License](LICENSE).
|
||||
Released under the [MIT License](LICENSE).
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import qlik from 'qlik';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ApplyPreMask } from '../masking';
|
||||
import { addSeparators } from '../utilities';
|
||||
import Tooltip from '../tooltip/index.jsx';
|
||||
|
||||
class DataCell extends React.PureComponent {
|
||||
@@ -11,23 +10,39 @@ class DataCell extends React.PureComponent {
|
||||
}
|
||||
|
||||
handleSelect () {
|
||||
const { data: { meta: { dimensionCount } }, general: { allowFilteringByClick }, measurement, qlik } = this.props;
|
||||
const {
|
||||
data: {
|
||||
headers,
|
||||
meta: {
|
||||
dimensionCount,
|
||||
altState
|
||||
}
|
||||
},
|
||||
general: {
|
||||
allowFilteringByClick
|
||||
},
|
||||
measurement,
|
||||
component
|
||||
} = this.props;
|
||||
|
||||
const hasSecondDimension = dimensionCount > 1;
|
||||
if (!allowFilteringByClick) {
|
||||
return;
|
||||
}
|
||||
|
||||
qlik.backendApi.selectValues(0, [measurement.parents.dimension1.elementNumber], true);
|
||||
const app = qlik.currApp(component);
|
||||
app.field(headers.dimension1[0].name, altState)
|
||||
.select([measurement.parents.dimension1.elementNumber], false, false);
|
||||
|
||||
if (hasSecondDimension) {
|
||||
qlik.backendApi.selectValues(1, [measurement.parents.dimension2.elementNumber], true);
|
||||
app.field(headers.dimension2[0].name, altState)
|
||||
.select([measurement.parents.dimension2.elementNumber], false, false);
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
data,
|
||||
general,
|
||||
cellWidth,
|
||||
measurement,
|
||||
styleBuilder,
|
||||
styling
|
||||
@@ -39,17 +54,16 @@ class DataCell extends React.PureComponent {
|
||||
fontFamily: styling.options.fontFamily,
|
||||
...styleBuilder.getStyle(),
|
||||
paddingLeft: '5px',
|
||||
textAlign: textAlignment
|
||||
textAlign: textAlignment,
|
||||
minWidth: cellWidth,
|
||||
maxWidth: cellWidth
|
||||
};
|
||||
|
||||
const isEmptyCell = measurement.displayValue === '';
|
||||
const isColumnPercentageBased = (/%/).test(measurement.format);
|
||||
let formattedMeasurementValue;
|
||||
if (isEmptyCell) {
|
||||
if (isEmptyCell || styleBuilder.hasComments()) {
|
||||
formattedMeasurementValue = '';
|
||||
cellStyle.cursor = 'default';
|
||||
} else if (styleBuilder.hasComments()) {
|
||||
formattedMeasurementValue = '.';
|
||||
} else {
|
||||
formattedMeasurementValue = formatMeasurementValue(measurement, styling);
|
||||
}
|
||||
@@ -70,16 +84,9 @@ class DataCell extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
let cellClass = 'grid-cells';
|
||||
const hasTwoDimensions = data.headers.dimension2 && data.headers.dimension2.length > 0;
|
||||
const shouldUseSmallCells = isColumnPercentageBased && data.headers.measurements.length > 1 && hasTwoDimensions;
|
||||
if (shouldUseSmallCells) {
|
||||
cellClass = 'grid-cells-small';
|
||||
}
|
||||
|
||||
return (
|
||||
<td
|
||||
className={`${cellClass}${general.cellSuffix}`}
|
||||
className="grid-cells"
|
||||
onClick={isEmptyCell ? null : this.handleSelect}
|
||||
style={cellStyle}
|
||||
>
|
||||
@@ -95,29 +102,24 @@ class DataCell extends React.PureComponent {
|
||||
}
|
||||
|
||||
DataCell.propTypes = {
|
||||
cellWidth: PropTypes.string.isRequired,
|
||||
data: PropTypes.shape({
|
||||
headers: PropTypes.shape({
|
||||
dimension1: PropTypes.array.isRequired,
|
||||
measurements: PropTypes.array.isRequired
|
||||
}).isRequired,
|
||||
meta: PropTypes.shape({
|
||||
altState: PropTypes.string.isRequired,
|
||||
dimensionCount: PropTypes.number.isRequired
|
||||
}).isRequired
|
||||
}).isRequired,
|
||||
general: PropTypes.shape({
|
||||
cellSuffix: PropTypes.string.isRequired
|
||||
}).isRequired,
|
||||
general: PropTypes.shape({}).isRequired,
|
||||
measurement: PropTypes.shape({
|
||||
format: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
value: PropTypes.any
|
||||
}).isRequired,
|
||||
qlik: PropTypes.shape({
|
||||
backendApi: PropTypes.shape({
|
||||
selectValues: function (props, propName) {
|
||||
if (props.isSnapshot || typeof props[propName] === 'function') {
|
||||
return null;
|
||||
}
|
||||
return new Error('Missing implementation of qlik.backendApi.selectValues.');
|
||||
}
|
||||
}).isRequired
|
||||
}).isRequired,
|
||||
component: PropTypes.shape({}).isRequired,
|
||||
styleBuilder: PropTypes.shape({
|
||||
hasComments: PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
@@ -129,51 +131,11 @@ DataCell.propTypes = {
|
||||
export default DataCell;
|
||||
|
||||
function formatMeasurementValue (measurement, styling) {
|
||||
const isColumnPercentageBased = (/%/).test(measurement.format);
|
||||
let formattedMeasurementValue = '';
|
||||
if (isColumnPercentageBased) {
|
||||
if (isNaN(measurement.value)) {
|
||||
formattedMeasurementValue = styling.symbolForNulls;
|
||||
} else {
|
||||
formattedMeasurementValue = ApplyPreMask('0,00%', measurement.value);
|
||||
}
|
||||
} else {
|
||||
let magnitudeDivider;
|
||||
switch (measurement.magnitude.toLowerCase()) {
|
||||
case 'k':
|
||||
magnitudeDivider = 1000;
|
||||
break;
|
||||
case 'm':
|
||||
magnitudeDivider = 1000000;
|
||||
break;
|
||||
default:
|
||||
magnitudeDivider = 1;
|
||||
}
|
||||
const formattingStringWithoutMagnitude = measurement.format.replace(/k|K|m|M/gi, '');
|
||||
if (isNaN(measurement.value)) {
|
||||
formattedMeasurementValue = styling.symbolForNulls;
|
||||
} else {
|
||||
let preFormatValue = measurement.value;
|
||||
if (isColumnPercentageBased) {
|
||||
preFormatValue *= 100;
|
||||
}
|
||||
switch (formattingStringWithoutMagnitude) {
|
||||
case '#.##0':
|
||||
formattedMeasurementValue = addSeparators((preFormatValue / magnitudeDivider), '.', ',', 0);
|
||||
break;
|
||||
case '#,##0':
|
||||
formattedMeasurementValue = addSeparators((preFormatValue / magnitudeDivider), ',', '.', 0);
|
||||
break;
|
||||
default:
|
||||
formattedMeasurementValue = ApplyPreMask(
|
||||
formattingStringWithoutMagnitude,
|
||||
(preFormatValue / magnitudeDivider)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isNaN(measurement.value)) {
|
||||
return styling.symbolForNulls;
|
||||
}
|
||||
return formattedMeasurementValue;
|
||||
|
||||
return measurement.displayValue;
|
||||
}
|
||||
|
||||
function getConditionalColor (measurement, conditionalColoring) {
|
||||
|
||||
@@ -5,101 +5,114 @@ import DataCell from './data-cell.jsx';
|
||||
import RowHeader from './row-header.jsx';
|
||||
import { injectSeparators } from '../utilities';
|
||||
|
||||
const DataTable = ({ data, general, qlik, renderData, styling }) => {
|
||||
const {
|
||||
headers: {
|
||||
dimension1,
|
||||
measurements
|
||||
},
|
||||
matrix
|
||||
} = data;
|
||||
class DataTable extends React.PureComponent {
|
||||
render () {
|
||||
const {
|
||||
cellWidth,
|
||||
columnSeparatorWidth,
|
||||
component,
|
||||
data,
|
||||
general,
|
||||
renderData,
|
||||
styling
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className="row-wrapper">
|
||||
<table>
|
||||
<tbody>
|
||||
{dimension1.map((dimensionEntry, dimensionIndex) => {
|
||||
const rowHeaderText = dimensionEntry.displayValue || '';
|
||||
if (rowHeaderText === '-') {
|
||||
return null;
|
||||
}
|
||||
const 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()
|
||||
};
|
||||
const {
|
||||
headers: {
|
||||
dimension1,
|
||||
measurements
|
||||
},
|
||||
matrix
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<tr key={dimensionEntry.displayValue}>
|
||||
{!renderData ?
|
||||
<RowHeader
|
||||
entry={dimensionEntry}
|
||||
qlik={qlik}
|
||||
rowStyle={rowStyle}
|
||||
styleBuilder={styleBuilder}
|
||||
styling={styling}
|
||||
/> : null
|
||||
}
|
||||
{renderData && injectSeparators(
|
||||
matrix[dimensionIndex],
|
||||
styling.useSeparatorColumns,
|
||||
{ atEvery: measurements.length }
|
||||
).map((measurementData, index) => {
|
||||
if (measurementData.isSeparator) {
|
||||
const separatorStyle = {
|
||||
color: 'white',
|
||||
fontFamily: styling.options.fontFamily,
|
||||
fontSize: `${12 + styling.options.fontSizeAdjustment}px`
|
||||
};
|
||||
const separatorStyle = {
|
||||
minWidth: columnSeparatorWidth,
|
||||
maxWidth: columnSeparatorWidth
|
||||
};
|
||||
|
||||
return (
|
||||
<td
|
||||
className="empty"
|
||||
key={`${dimensionEntry.displayValue}-${index}-separator`}
|
||||
style={separatorStyle}
|
||||
>
|
||||
*
|
||||
</td>
|
||||
);
|
||||
}
|
||||
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()
|
||||
};
|
||||
|
||||
const { dimension1: dimension1Info, dimension2, measurement } = measurementData.parents;
|
||||
const id = `${dimension1Info.elementNumber}-${dimension2 && dimension2.elementNumber}-${measurement.header}`;
|
||||
return (
|
||||
<DataCell
|
||||
data={data}
|
||||
general={general}
|
||||
key={`${dimensionEntry.displayValue}-${id}`}
|
||||
measurement={measurementData}
|
||||
qlik={qlik}
|
||||
return (
|
||||
<tr key={dimensionEntry.displayValue}>
|
||||
{!renderData ?
|
||||
<RowHeader
|
||||
altState={data.meta.altState}
|
||||
component={component}
|
||||
entry={dimensionEntry}
|
||||
rowStyle={rowStyle}
|
||||
styleBuilder={styleBuilder}
|
||||
styling={styling}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
/> : null
|
||||
}
|
||||
{renderData && injectSeparators(
|
||||
matrix[dimensionIndex],
|
||||
columnSeparatorWidth,
|
||||
{ atEvery: measurements.length }
|
||||
).map((measurementData, index) => {
|
||||
if (measurementData.isSeparator) {
|
||||
return (
|
||||
<td
|
||||
className="empty"
|
||||
key={`${dimensionEntry.displayValue}-${index}-separator`}
|
||||
style={separatorStyle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { dimension1: dimension1Info, dimension2, measurement } = measurementData.parents;
|
||||
const id = `${dimension1Info.elementNumber}-${dimension2 && dimension2.elementNumber}-${measurement.header}-${measurement.index}`;
|
||||
return (
|
||||
<DataCell
|
||||
cellWidth={cellWidth}
|
||||
component={component}
|
||||
data={data}
|
||||
general={general}
|
||||
key={`${dimensionEntry.displayValue}-${id}`}
|
||||
measurement={measurementData}
|
||||
styleBuilder={styleBuilder}
|
||||
styling={styling}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DataTable.defaultProps = {
|
||||
renderData: true
|
||||
};
|
||||
|
||||
DataTable.propTypes = {
|
||||
cellWidth: PropTypes.string.isRequired,
|
||||
columnSeparatorWidth: PropTypes.string.isRequired,
|
||||
data: PropTypes.shape({
|
||||
headers: PropTypes.shape({
|
||||
dimension1: PropTypes.array.isRequired
|
||||
@@ -107,7 +120,7 @@ DataTable.propTypes = {
|
||||
matrix: PropTypes.arrayOf(PropTypes.array.isRequired).isRequired
|
||||
}).isRequired,
|
||||
general: PropTypes.shape({}).isRequired,
|
||||
qlik: PropTypes.shape({}).isRequired,
|
||||
component: PropTypes.shape({}).isRequired,
|
||||
renderData: PropTypes.bool,
|
||||
styling: PropTypes.shape({
|
||||
hasCustomFileStyle: PropTypes.bool.isRequired
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import qlik from 'qlik';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import HeaderPadding from './header-padding.jsx';
|
||||
@@ -11,13 +12,14 @@ class RowHeader extends React.PureComponent {
|
||||
}
|
||||
|
||||
handleSelect () {
|
||||
const { entry, qlik } = this.props;
|
||||
qlik.backendApi.selectValues(0, [entry.elementNumber], true);
|
||||
const { entry, altState, component } = this.props;
|
||||
const app = qlik.currApp(component);
|
||||
app.field(entry.name, altState).select([entry.elementNumber], false, false);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { entry, rowStyle, styleBuilder, styling, qlik } = this.props;
|
||||
const inEditState = qlik.inEditState();
|
||||
const { entry, rowStyle, styleBuilder, styling, component } = this.props;
|
||||
const inEditState = component.inEditState();
|
||||
|
||||
return (
|
||||
<td
|
||||
@@ -43,18 +45,12 @@ class RowHeader extends React.PureComponent {
|
||||
|
||||
RowHeader.propTypes = {
|
||||
entry: PropTypes.shape({
|
||||
displayValue: PropTypes.string.isRequired
|
||||
}).isRequired,
|
||||
qlik: PropTypes.shape({
|
||||
backendApi: PropTypes.shape({
|
||||
selectValues: function (props, propName) {
|
||||
if (props.isSnapshot || typeof props[propName] === 'function') {
|
||||
return null;
|
||||
}
|
||||
return new Error('Missing implementation of qlik.backendApi.selectValues.');
|
||||
}
|
||||
}).isRequired
|
||||
displayValue: PropTypes.string.isRequired,
|
||||
elementNumber: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired
|
||||
}).isRequired,
|
||||
altState: PropTypes.string.isRequired,
|
||||
component: PropTypes.shape({}).isRequired,
|
||||
rowStyle: PropTypes.shape({}).isRequired,
|
||||
styleBuilder: PropTypes.shape({}).isRequired,
|
||||
styling: PropTypes.shape({}).isRequired
|
||||
|
||||
@@ -22,7 +22,9 @@ async function buildDataCube (originCubeDefinition, hasTwoDimensions, app) {
|
||||
cubeDefinition.qDimensions.push(originCubeDefinition.qDimensions[1]);
|
||||
}
|
||||
const cube = await createCube(cubeDefinition, app);
|
||||
return cube.qHyperCube.qDataPages[0].qMatrix;
|
||||
const cubeMatrix = cube.qHyperCube.qDataPages[0].qMatrix;
|
||||
app.destroySessionObject(cube.qInfo.qId);
|
||||
return cubeMatrix;
|
||||
}
|
||||
|
||||
export async function initializeDataCube (component, layout) {
|
||||
@@ -37,6 +39,7 @@ export async function initializeDataCube (component, layout) {
|
||||
const hyperCubeDef = properties.qExtendsId
|
||||
? (await app.getObjectProperties(properties.qExtendsId)).properties.qHyperCubeDef
|
||||
: properties.qHyperCubeDef;
|
||||
hyperCubeDef.qStateName = layout.qStateName || "";
|
||||
|
||||
return buildDataCube(hyperCubeDef, layout.qHyperCube.qDimensionInfo.length === 2, app);
|
||||
}
|
||||
|
||||
@@ -209,15 +209,33 @@ const tableFormat = {
|
||||
],
|
||||
defaultValue: 'right'
|
||||
},
|
||||
FitChartWidth: {
|
||||
ref: 'fitchartwidth',
|
||||
type: 'boolean',
|
||||
component: 'switch',
|
||||
label: 'Fill chart width',
|
||||
options: [
|
||||
{
|
||||
value: true,
|
||||
label: 'On'
|
||||
},
|
||||
{
|
||||
value: false,
|
||||
label: 'Off'
|
||||
}
|
||||
],
|
||||
defaultValue: false
|
||||
},
|
||||
ColumnWidthSlider: {
|
||||
type: 'number',
|
||||
component: 'slider',
|
||||
label: 'Column width',
|
||||
ref: 'columnwidthslider',
|
||||
min: 1,
|
||||
max: 3,
|
||||
step: 1,
|
||||
defaultValue: 2
|
||||
min: 20,
|
||||
max: 250,
|
||||
step: 10,
|
||||
defaultValue: 50,
|
||||
show: data => !data.fitchartwidth
|
||||
},
|
||||
SymbolForNulls: {
|
||||
ref: 'symbolfornulls',
|
||||
|
||||
@@ -7,13 +7,12 @@ function cleanupNodes (node) {
|
||||
});
|
||||
}
|
||||
|
||||
function buildTableHTML (id, title, subtitle, footnote) {
|
||||
function buildTableHTML (containerElement, title, subtitle, footnote) {
|
||||
const titleHTML = `<p style="font-size:15pt"><b>${title}</b></p>`;
|
||||
const subtitleHTML = `<p style="font-size:11pt">${subtitle}</p>`;
|
||||
const footnoteHTML = `<p style="font-size:11pt">${footnote}</p>`;
|
||||
const container = document.querySelector(`[tid="${id}"]`);
|
||||
const kpiTableClone = container.querySelector('.kpi-table').cloneNode(true);
|
||||
const dataTableClone = container.querySelector('.data-table').cloneNode(true);
|
||||
const kpiTableClone = containerElement[0].querySelector('.kpi-table').cloneNode(true);
|
||||
const dataTableClone = containerElement[0].querySelector('.data-table').cloneNode(true);
|
||||
cleanupNodes(kpiTableClone);
|
||||
cleanupNodes(kpiTableClone);
|
||||
|
||||
@@ -83,7 +82,7 @@ function buildTableHTML (id, title, subtitle, footnote) {
|
||||
|
||||
function downloadXLS (html) {
|
||||
const filename = 'analysis.xls';
|
||||
const blobObject = new Blob([html]);
|
||||
const blobObject = new Blob([html], { type: 'application/vnd.ms-excel' });
|
||||
|
||||
// IE/Edge
|
||||
if (window.navigator.msSaveOrOpenBlob) {
|
||||
@@ -100,8 +99,8 @@ function downloadXLS (html) {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function exportXLS (id, title, subtitle, footnote) {
|
||||
export function exportXLS (containerElement, title, subtitle, footnote) {
|
||||
// original was removing icon when starting export, disable and some spinner instead, shouldn't take enough time to warrant either..?
|
||||
const table = buildTableHTML(id, title, subtitle, footnote);
|
||||
const table = buildTableHTML(containerElement, title, subtitle, footnote);
|
||||
downloadXLS(table);
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ class ExportButton extends React.PureComponent {
|
||||
}
|
||||
|
||||
handleExport () {
|
||||
const { id, excelExport, general } = this.props;
|
||||
const { component, excelExport, general } = this.props;
|
||||
const { title, subtitle, footnote } = general;
|
||||
if (excelExport) {
|
||||
exportXLS(id, title, subtitle, footnote);
|
||||
exportXLS(component.$element, title, subtitle, footnote);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ ExportButton.defaultProps = {
|
||||
};
|
||||
|
||||
ExportButton.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
component: PropTypes.shape({}).isRequired,
|
||||
excelExport: PropTypes.bool,
|
||||
general: PropTypes.shape({}).isRequired
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import qlik from 'qlik';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { HEADER_FONT_SIZE } from '../initialize-transformed';
|
||||
@@ -10,25 +11,28 @@ class ColumnHeader extends React.PureComponent {
|
||||
}
|
||||
|
||||
handleSelect () {
|
||||
const { entry, qlik } = this.props;
|
||||
qlik.backendApi.selectValues(1, [entry.elementNumber], true);
|
||||
const { entry, altState, component } = this.props;
|
||||
const app = qlik.currApp(component);
|
||||
app.field(entry.name, altState).select([entry.elementNumber], false, false);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { baseCSS, cellSuffix, colSpan, entry, styling, qlik } = this.props;
|
||||
const inEditState = qlik.inEditState();
|
||||
const { baseCSS, cellWidth, colSpan, component, entry, styling } = this.props;
|
||||
const inEditState = component.inEditState();
|
||||
const isMediumFontSize = styling.headerOptions.fontSizeAdjustment === HEADER_FONT_SIZE.MEDIUM;
|
||||
|
||||
const style = {
|
||||
...baseCSS,
|
||||
fontSize: `${14 + styling.headerOptions.fontSizeAdjustment}px`,
|
||||
height: isMediumFontSize ? '43px' : '33px',
|
||||
verticalAlign: 'middle'
|
||||
verticalAlign: 'middle',
|
||||
minWidth: cellWidth,
|
||||
maxWidth: cellWidth
|
||||
};
|
||||
|
||||
return (
|
||||
<th
|
||||
className={`grid-cells2${cellSuffix}`}
|
||||
className="grid-cells"
|
||||
colSpan={colSpan}
|
||||
onClick={this.handleSelect}
|
||||
style={style}
|
||||
@@ -46,28 +50,20 @@ class ColumnHeader extends React.PureComponent {
|
||||
}
|
||||
|
||||
ColumnHeader.defaultProps = {
|
||||
cellSuffix: '',
|
||||
colSpan: 1
|
||||
};
|
||||
|
||||
ColumnHeader.propTypes = {
|
||||
baseCSS: PropTypes.shape({}).isRequired,
|
||||
cellSuffix: PropTypes.string,
|
||||
cellWidth: PropTypes.string.isRequired,
|
||||
colSpan: PropTypes.number,
|
||||
entry: PropTypes.shape({
|
||||
displayValue: PropTypes.string.isRequired,
|
||||
elementNumber: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired
|
||||
}).isRequired,
|
||||
qlik: PropTypes.shape({
|
||||
backendApi: PropTypes.shape({
|
||||
selectValues: function (props, propName) {
|
||||
if (props.isSnapshot || typeof props[propName] === 'function') {
|
||||
return null;
|
||||
}
|
||||
return new Error('Missing implementation of qlik.backendApi.selectValues.');
|
||||
}
|
||||
}).isRequired
|
||||
}).isRequired,
|
||||
altState: PropTypes.string.isRequired,
|
||||
component: PropTypes.shape({}).isRequired,
|
||||
styling: PropTypes.shape({
|
||||
headerOptions: PropTypes.shape({
|
||||
fontSizeAdjustment: PropTypes.number.isRequired
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import ExportButton from '../export-button.jsx';
|
||||
import { HEADER_FONT_SIZE } from '../initialize-transformed';
|
||||
|
||||
const ExportColumnHeader = ({ id, baseCSS, general, title, allowExcelExport, hasSecondDimension, styling }) => {
|
||||
const ExportColumnHeader = ({ component, baseCSS, general, title, allowExcelExport, hasSecondDimension, styling }) => {
|
||||
const rowSpan = hasSecondDimension ? 2 : 1;
|
||||
const isMediumFontSize = styling.headerOptions.fontSizeAdjustment === HEADER_FONT_SIZE.MEDIUM;
|
||||
const style = {
|
||||
@@ -22,7 +22,7 @@ const ExportColumnHeader = ({ id, baseCSS, general, title, allowExcelExport, has
|
||||
style={style}
|
||||
>
|
||||
<ExportButton
|
||||
id={id}
|
||||
component={component}
|
||||
excelExport={allowExcelExport}
|
||||
general={general}
|
||||
/>
|
||||
@@ -32,7 +32,7 @@ const ExportColumnHeader = ({ id, baseCSS, general, title, allowExcelExport, has
|
||||
};
|
||||
|
||||
ExportColumnHeader.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
component: PropTypes.shape({}).isRequired,
|
||||
allowExcelExport: PropTypes.bool.isRequired,
|
||||
baseCSS: PropTypes.shape({}).isRequired,
|
||||
general: PropTypes.shape({}).isRequired,
|
||||
|
||||
@@ -5,138 +5,136 @@ import ColumnHeader from './column-header.jsx';
|
||||
import MeasurementColumnHeader from './measurement-column-header.jsx';
|
||||
import { injectSeparators } from '../utilities';
|
||||
|
||||
const HeadersTable = ({ data, general, qlik, styling, isKpi }) => {
|
||||
const baseCSS = {
|
||||
backgroundColor: styling.headerOptions.colorSchema,
|
||||
color: styling.headerOptions.textColor,
|
||||
fontFamily: styling.options.fontFamily,
|
||||
textAlign: styling.headerOptions.alignment
|
||||
};
|
||||
class HeadersTable extends React.PureComponent {
|
||||
render () {
|
||||
const {
|
||||
cellWidth,
|
||||
columnSeparatorWidth,
|
||||
component,
|
||||
data,
|
||||
general,
|
||||
isKpi,
|
||||
styling
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
dimension1,
|
||||
dimension2,
|
||||
measurements
|
||||
} = data.headers;
|
||||
const baseCSS = {
|
||||
backgroundColor: styling.headerOptions.colorSchema,
|
||||
color: styling.headerOptions.textColor,
|
||||
fontFamily: styling.options.fontFamily,
|
||||
textAlign: styling.headerOptions.alignment
|
||||
};
|
||||
|
||||
const hasSecondDimension = dimension2.length > 0;
|
||||
const {
|
||||
dimension1,
|
||||
dimension2,
|
||||
measurements
|
||||
} = data.headers;
|
||||
|
||||
return (
|
||||
<div className="header-wrapper">
|
||||
<table className="header">
|
||||
<tbody>
|
||||
<tr>
|
||||
{isKpi ?
|
||||
<ExportColumnHeader
|
||||
id={qlik.options.id}
|
||||
allowExcelExport={general.allowExcelExport}
|
||||
baseCSS={baseCSS}
|
||||
general={general}
|
||||
hasSecondDimension={hasSecondDimension}
|
||||
styling={styling}
|
||||
title={dimension1[0].name}
|
||||
/> : null
|
||||
}
|
||||
{!isKpi && !hasSecondDimension && measurements.map(measurementEntry => (
|
||||
<MeasurementColumnHeader
|
||||
baseCSS={baseCSS}
|
||||
general={general}
|
||||
hasSecondDimension={hasSecondDimension}
|
||||
key={`${measurementEntry.displayValue}-${measurementEntry.name}`}
|
||||
measurement={measurementEntry}
|
||||
styling={styling}
|
||||
/>
|
||||
))}
|
||||
{!isKpi && hasSecondDimension && injectSeparators(dimension2, styling.useSeparatorColumns).map((entry, index) => {
|
||||
if (entry.isSeparator) {
|
||||
const separatorStyle = {
|
||||
color: 'white',
|
||||
fontFamily: styling.options.fontFamily,
|
||||
fontSize: `${13 + styling.headerOptions.fontSizeAdjustment}px`
|
||||
};
|
||||
const hasSecondDimension = dimension2.length > 0;
|
||||
|
||||
return (
|
||||
<th
|
||||
className="empty"
|
||||
key={index}
|
||||
style={separatorStyle}
|
||||
>
|
||||
*
|
||||
</th>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ColumnHeader
|
||||
const separatorStyle = {
|
||||
minWidth: columnSeparatorWidth,
|
||||
maxWidth: columnSeparatorWidth
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="header-wrapper">
|
||||
<table className="header">
|
||||
<tbody>
|
||||
<tr>
|
||||
{isKpi ?
|
||||
<ExportColumnHeader
|
||||
allowExcelExport={general.allowExcelExport}
|
||||
baseCSS={baseCSS}
|
||||
cellSuffix={general.cellSuffix}
|
||||
colSpan={measurements.length}
|
||||
entry={entry}
|
||||
key={entry.displayValue}
|
||||
qlik={qlik}
|
||||
component={component}
|
||||
general={general}
|
||||
hasSecondDimension={hasSecondDimension}
|
||||
styling={styling}
|
||||
title={dimension1[0].name}
|
||||
/> : null
|
||||
}
|
||||
{!isKpi && !hasSecondDimension && measurements.map(measurementEntry => (
|
||||
<MeasurementColumnHeader
|
||||
baseCSS={baseCSS}
|
||||
cellWidth={cellWidth}
|
||||
hasSecondDimension={hasSecondDimension}
|
||||
key={`${measurementEntry.displayValue}-${measurementEntry.name}-${measurementEntry.index}`}
|
||||
measurement={measurementEntry}
|
||||
styling={styling}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
{!isKpi && hasSecondDimension && (
|
||||
<tr>
|
||||
{injectSeparators(dimension2, styling.useSeparatorColumns).map((dimensionEntry, index) => {
|
||||
if (dimensionEntry.isSeparator) {
|
||||
const separatorStyle = {
|
||||
color: 'white',
|
||||
fontFamily: styling.options.fontFamily,
|
||||
fontSize: `${12 + styling.headerOptions.fontSizeAdjustment}px`
|
||||
};
|
||||
|
||||
))}
|
||||
{!isKpi && hasSecondDimension && injectSeparators(dimension2, columnSeparatorWidth).map((entry, index) => {
|
||||
if (entry.isSeparator) {
|
||||
return (
|
||||
<th
|
||||
className="empty"
|
||||
key={index}
|
||||
style={separatorStyle}
|
||||
>
|
||||
*
|
||||
</th>
|
||||
/>
|
||||
);
|
||||
}
|
||||
return measurements.map(measurementEntry => (
|
||||
<MeasurementColumnHeader
|
||||
return (
|
||||
<ColumnHeader
|
||||
altState={data.meta.altState}
|
||||
baseCSS={baseCSS}
|
||||
dimensionEntry={dimensionEntry}
|
||||
general={general}
|
||||
hasSecondDimension={hasSecondDimension}
|
||||
key={`${measurementEntry.displayValue}-${measurementEntry.name}-${dimensionEntry.name}`}
|
||||
measurement={measurementEntry}
|
||||
cellWidth={cellWidth}
|
||||
colSpan={measurements.length}
|
||||
component={component}
|
||||
entry={entry}
|
||||
key={entry.displayValue}
|
||||
styling={styling}
|
||||
/>
|
||||
));
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
{!isKpi && hasSecondDimension && (
|
||||
<tr>
|
||||
{injectSeparators(dimension2, columnSeparatorWidth).map((dimensionEntry, index) => {
|
||||
if (dimensionEntry.isSeparator) {
|
||||
return (
|
||||
<th
|
||||
className="empty"
|
||||
key={index}
|
||||
style={separatorStyle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return measurements.map(measurementEntry => (
|
||||
<MeasurementColumnHeader
|
||||
baseCSS={baseCSS}
|
||||
cellWidth={cellWidth}
|
||||
dimensionEntry={dimensionEntry}
|
||||
hasSecondDimension={hasSecondDimension}
|
||||
key={`${measurementEntry.displayValue}-${measurementEntry.name}-${measurementEntry.index}-${dimensionEntry.name}`}
|
||||
measurement={measurementEntry}
|
||||
styling={styling}
|
||||
/>
|
||||
));
|
||||
})}
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
HeadersTable.propTypes = {
|
||||
cellWidth: PropTypes.string.isRequired,
|
||||
columnSeparatorWidth: PropTypes.string.isRequired,
|
||||
data: PropTypes.shape({
|
||||
headers: PropTypes.shape({
|
||||
dimension1: PropTypes.array,
|
||||
dimension2: PropTypes.array,
|
||||
measurements: PropTypes.array
|
||||
}),
|
||||
meta: PropTypes.shape({
|
||||
altState: PropTypes.string.isRequired
|
||||
})
|
||||
}).isRequired,
|
||||
general: PropTypes.shape({}).isRequired,
|
||||
qlik: PropTypes.shape({
|
||||
backendApi: PropTypes.shape({
|
||||
selectValues: function (props, propName) {
|
||||
if (props.isSnapshot || typeof props[propName] === 'function') {
|
||||
return null;
|
||||
}
|
||||
return new Error('Missing implementation of qlik.backendApi.selectValues.');
|
||||
}
|
||||
}).isRequired
|
||||
}).isRequired,
|
||||
component: PropTypes.shape({}).isRequired,
|
||||
styling: PropTypes.shape({
|
||||
headerOptions: PropTypes.shape({}),
|
||||
options: PropTypes.shape({})
|
||||
|
||||
@@ -3,28 +3,30 @@ import PropTypes from 'prop-types';
|
||||
import { HEADER_FONT_SIZE } from '../initialize-transformed';
|
||||
import Tooltip from '../tooltip/index.jsx';
|
||||
|
||||
const MeasurementColumnHeader = ({ baseCSS, general, hasSecondDimension, measurement, styling }) => {
|
||||
const title = `${measurement.name} ${measurement.magnitudeLabelSuffix}`;
|
||||
const MeasurementColumnHeader = ({ baseCSS, cellWidth, hasSecondDimension, measurement, styling }) => {
|
||||
const title = `${measurement.name}`;
|
||||
const { fontSizeAdjustment } = styling.headerOptions;
|
||||
const isMediumFontSize = fontSizeAdjustment === HEADER_FONT_SIZE.MEDIUM;
|
||||
|
||||
const cellStyle = {
|
||||
...baseCSS,
|
||||
verticalAlign: 'middle',
|
||||
minWidth: cellWidth,
|
||||
maxWidth: cellWidth
|
||||
};
|
||||
|
||||
if (hasSecondDimension) {
|
||||
const isPercentageFormat = measurement.format.substring(measurement.format.length - 1) === '%';
|
||||
let baseFontSize = 14;
|
||||
let cellClass = 'grid-cells2';
|
||||
if (isPercentageFormat) {
|
||||
baseFontSize = 13;
|
||||
cellClass = 'grid-cells2-small';
|
||||
}
|
||||
const cellStyle = {
|
||||
...baseCSS,
|
||||
fontSize: `${baseFontSize + fontSizeAdjustment}px`,
|
||||
height: isMediumFontSize ? '45px' : '35px',
|
||||
verticalAlign: 'middle'
|
||||
};
|
||||
cellStyle.fontSize = `${baseFontSize + fontSizeAdjustment}px`;
|
||||
cellStyle.height = isMediumFontSize ? '45px' : '35px';
|
||||
|
||||
return (
|
||||
<th
|
||||
className={`${cellClass}${general.cellSuffix}`}
|
||||
className="grid-cells"
|
||||
style={cellStyle}
|
||||
>
|
||||
<Tooltip
|
||||
@@ -37,16 +39,12 @@ const MeasurementColumnHeader = ({ baseCSS, general, hasSecondDimension, measure
|
||||
);
|
||||
}
|
||||
|
||||
const style = {
|
||||
...baseCSS,
|
||||
fontSize: `${15 + fontSizeAdjustment}px`,
|
||||
height: isMediumFontSize ? '90px' : '70px',
|
||||
verticalAlign: 'middle'
|
||||
};
|
||||
cellStyle.fontSize = `${15 + fontSizeAdjustment}px`;
|
||||
cellStyle.height = isMediumFontSize ? '90px' : '70px';
|
||||
return (
|
||||
<th
|
||||
className={`grid-cells2${general.cellSuffix}`}
|
||||
style={style}
|
||||
className="grid-cells"
|
||||
style={cellStyle}
|
||||
>
|
||||
<Tooltip
|
||||
tooltipText={title}
|
||||
@@ -64,9 +62,7 @@ MeasurementColumnHeader.defaultProps = {
|
||||
|
||||
MeasurementColumnHeader.propTypes = {
|
||||
baseCSS: PropTypes.shape({}).isRequired,
|
||||
general: PropTypes.shape({
|
||||
cellSuffix: PropTypes.string.isRequired
|
||||
}).isRequired,
|
||||
cellWidth: PropTypes.string.isRequired,
|
||||
hasSecondDimension: PropTypes.bool,
|
||||
measurement: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired
|
||||
|
||||
@@ -71,7 +71,7 @@ export default {
|
||||
const jsx = (
|
||||
<Root
|
||||
editmodeClass={editmodeClass}
|
||||
qlik={this}
|
||||
component={this}
|
||||
state={state}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -24,40 +24,10 @@ function getFontSizeAdjustment (option) {
|
||||
return fontSizeAdjustmentOptions[option] || 0;
|
||||
}
|
||||
|
||||
function getCellSuffix (option) {
|
||||
const cellSuffixOptions = {
|
||||
1: '-s',
|
||||
3: '-l'
|
||||
};
|
||||
|
||||
return cellSuffixOptions[option] || '';
|
||||
}
|
||||
|
||||
function getMeasurementFormat (measurement) {
|
||||
if (measurement.qNumFormat.qType === 'U' || measurement.qNumFormat.qFmt === '##############') {
|
||||
return '#.##0';
|
||||
} else if (measurement.qNumFormat.qType === 'R') {
|
||||
return measurement.qNumFormat.qFmt.replace(/(|)/gi, '');
|
||||
}
|
||||
return measurement.qNumFormat.qFmt;
|
||||
}
|
||||
|
||||
function getMagnitudeLabelSuffix (magnitudeOption) {
|
||||
const magnitudeLabelSuffixOptions = {
|
||||
'k': ' (k)',
|
||||
'm': ' (m)'
|
||||
};
|
||||
|
||||
return magnitudeLabelSuffixOptions[magnitudeOption] || '';
|
||||
}
|
||||
|
||||
function generateMeasurements (information) {
|
||||
return information.map(measurement => {
|
||||
const format = getMeasurementFormat(measurement);
|
||||
const formatMagnitude = format.substr(format.length - 1).toLowerCase();
|
||||
const transformedMeasurement = {
|
||||
format,
|
||||
magnitudeLabelSuffix: getMagnitudeLabelSuffix(formatMagnitude),
|
||||
format: measurement.qNumFormat.qFmt || '#.##0',
|
||||
name: measurement.qFallbackTitle
|
||||
};
|
||||
|
||||
@@ -78,11 +48,6 @@ function generateMatrixCell ({ cell, dimension1Information, dimension2Informatio
|
||||
const matrixCell = {
|
||||
displayValue: cell.qText,
|
||||
format: measurementInformation.format,
|
||||
magnitude: measurementInformation.magnitudeLabelSuffix.substring(
|
||||
measurementInformation.magnitudeLabelSuffix.length - 2,
|
||||
measurementInformation.magnitudeLabelSuffix.length - 1
|
||||
),
|
||||
magnitudeLabelSuffix: measurementInformation.magnitudeLabelSuffix,
|
||||
name: measurementInformation.name,
|
||||
parents: {
|
||||
dimension1: {
|
||||
@@ -262,6 +227,18 @@ function initializeTransformed ({ $element, component, dataCube, designList, lay
|
||||
}
|
||||
}
|
||||
|
||||
let cellWidth;
|
||||
if (layout.fitchartwidth) {
|
||||
// The widths are calculated based on the current element width. Note: this could use % to set
|
||||
// the widths as percentages of the available width. However, this often results in random
|
||||
// columns getting 1px wider than the others because of rounding necessary to fill the width.
|
||||
// This 1px causes missalignment between the data- and header tables.
|
||||
cellWidth = '';
|
||||
} else {
|
||||
// If using the previous solution just set 60px
|
||||
cellWidth = `${layout.columnwidthslider > 10 ? layout.columnwidthslider : 60}px`;
|
||||
}
|
||||
|
||||
// top level properties could be reducers and then components connect to grab what they want,
|
||||
// possibly with reselect for some presentational transforms (moving some of the presentational logic like formatting and such)
|
||||
const transformedProperties = {
|
||||
@@ -273,18 +250,20 @@ function initializeTransformed ({ $element, component, dataCube, designList, lay
|
||||
},
|
||||
matrix, // 2d array of all rows/cells to render in body of datatable
|
||||
meta: {
|
||||
dimensionCount: dimensionsInformation.length
|
||||
dimensionCount: dimensionsInformation.length,
|
||||
altState: layout.qStateName || ""
|
||||
}
|
||||
},
|
||||
general: {
|
||||
allowExcelExport: layout.allowexportxls,
|
||||
allowFilteringByClick: layout.filteroncellclick,
|
||||
cellSuffix: getCellSuffix(layout.columnwidthslider), // TOOD: move to matrix cells or is it headers.measurements?
|
||||
cellWidth: cellWidth,
|
||||
errorMessage: layout.errormessage,
|
||||
footnote: layout.footnote,
|
||||
maxLoops,
|
||||
subtitle: layout.subtitle,
|
||||
title: layout.title
|
||||
title: layout.title,
|
||||
useColumnSeparator: layout.separatorcols && dimensionCount > 1
|
||||
},
|
||||
selection: {
|
||||
dimensionSelectionCounts: dimensionsInformation.map(dimensionInfo => dimensionInfo.qStateCounts.qSelected)
|
||||
@@ -338,8 +317,7 @@ function initializeTransformed ({ $element, component, dataCube, designList, lay
|
||||
}
|
||||
},
|
||||
symbolForNulls: layout.symbolfornulls,
|
||||
usePadding: layout.indentbool,
|
||||
useSeparatorColumns: dimensionCount === 1 ? false : layout.separatorcols
|
||||
usePadding: layout.indentbool
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -12,9 +12,7 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
._cell(@Width: 50px) {
|
||||
min-width: @Width !important;
|
||||
max-width: @Width !important;
|
||||
.grid-cells {
|
||||
cursor: pointer;
|
||||
line-height: 1em !important;
|
||||
}
|
||||
@@ -59,10 +57,8 @@
|
||||
}
|
||||
|
||||
.empty {
|
||||
width: 3%;
|
||||
background: #fff;
|
||||
min-width: 4px !important;
|
||||
max-width: 4px !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
th.main-kpi {
|
||||
@@ -74,67 +70,6 @@
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
// *****************
|
||||
// Medium column size
|
||||
// *****************
|
||||
|
||||
.grid-cells {
|
||||
position: relative;
|
||||
._cell(70px);
|
||||
}
|
||||
|
||||
.grid-cells2 {
|
||||
._cell(70px);
|
||||
}
|
||||
|
||||
.grid-cells-small {
|
||||
._cell(52px);
|
||||
}
|
||||
|
||||
.grid-cells2-small {
|
||||
._cell(52px);
|
||||
}
|
||||
|
||||
// *****************
|
||||
// Small column size
|
||||
// *****************
|
||||
.grid-cells-s {
|
||||
._cell(67px);
|
||||
}
|
||||
|
||||
.grid-cells2-s {
|
||||
._cell(67px);
|
||||
}
|
||||
|
||||
.grid-cells-small-s {
|
||||
._cell(52px);
|
||||
}
|
||||
|
||||
.grid-cells2-small-s {
|
||||
._cell(52px);
|
||||
}
|
||||
|
||||
// *****************
|
||||
// Large column size
|
||||
// *****************
|
||||
.grid-cells-l {
|
||||
._cell(82px);
|
||||
}
|
||||
|
||||
.grid-cells2-l {
|
||||
._cell(82px);
|
||||
}
|
||||
|
||||
.grid-cells-small-l {
|
||||
._cell(66px);
|
||||
}
|
||||
|
||||
.grid-cells2-small-l {
|
||||
._cell(66px);
|
||||
}
|
||||
|
||||
// END OF GRID CELLS
|
||||
|
||||
// First Column
|
||||
.fdim-cells {
|
||||
min-width: 230px !Important;
|
||||
@@ -149,14 +84,6 @@
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.grid-cells-header {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.grid-cells-title {
|
||||
min-width: 522px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
height: 50px;
|
||||
width: 350px;
|
||||
|
||||
132
src/masking.js
132
src/masking.js
@@ -1,132 +0,0 @@
|
||||
import { addSeparators } from './utilities';
|
||||
|
||||
export function ApplyPreMask (mask, value) { // aqui
|
||||
if (mask.indexOf(';') >= 0) {
|
||||
if (value >= 0) {
|
||||
switch (mask.substring(0, mask.indexOf(';'))) {
|
||||
case '#.##0':
|
||||
return (addSeparators(value, '.', ',', 0));
|
||||
case '#,##0':
|
||||
return (addSeparators(value, ',', '.', 0));
|
||||
case '+#.##0':
|
||||
return (addSeparators(value, '.', ',', 0));
|
||||
case '+#,##0':
|
||||
return (addSeparators(value, ',', '.', 0));
|
||||
default:
|
||||
return (applyMask(mask.substring(0, mask.indexOf(';')), value));
|
||||
}
|
||||
} else {
|
||||
const vMyValue = value * -1;
|
||||
let vMyMask = mask.substring(mask.indexOf(';') + 1, mask.length);
|
||||
vMyMask = vMyMask.replace('(', '');
|
||||
vMyMask = vMyMask.replace(')', '');
|
||||
switch (vMyMask) {
|
||||
case '#.##0':
|
||||
return (`(${addSeparators(vMyValue, '.', ',', 0)})`);
|
||||
case '#,##0':
|
||||
return (`(${addSeparators(vMyValue, ',', '.', 0)})`);
|
||||
case '-#.##0':
|
||||
return (`(${addSeparators(vMyValue, '.', ',', 0)})`);
|
||||
case '-#,##0':
|
||||
return (`(${addSeparators(vMyValue, ',', '.', 0)})`);
|
||||
default:
|
||||
return (`(${applyMask(vMyMask, vMyValue)})`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return (applyMask(mask, value));
|
||||
}
|
||||
}
|
||||
|
||||
function applyMask (originalMask, originalValue) {
|
||||
if (!originalMask || isNaN(Number(originalValue))) {
|
||||
return originalValue;
|
||||
}
|
||||
|
||||
let isNegative;
|
||||
let result;
|
||||
let integer;
|
||||
// find prefix/suffix
|
||||
let len = originalMask.length;
|
||||
const start = originalMask.search(/[0-9\-\+#]/);
|
||||
const prefix = start > 0 ? originalMask.substring(0, start) : '';
|
||||
// reverse string: not an ideal method if there are surrogate pairs
|
||||
let str = originalMask.split('')
|
||||
.reverse()
|
||||
.join('');
|
||||
const end = str.search(/[0-9\-\+#]/);
|
||||
let offset = len - end;
|
||||
const substr = originalMask.substring(offset, offset + 1);
|
||||
let index = offset + ((substr === '.' || (substr === ',')) ? 1 : 0);
|
||||
const suffix = end > 0 ? originalMask.substring(index, len) : '';
|
||||
|
||||
// mask with prefix & suffix removed
|
||||
let mask = originalMask.substring(start, index);
|
||||
|
||||
// convert any string to number according to formation sign.
|
||||
let value = mask.charAt(0) === '-' ? -originalValue : Number(originalValue);
|
||||
isNegative = value < 0 ? value = -value : 0; // process only abs(), and turn on flag.
|
||||
|
||||
// search for separator for grp & decimal, anything not digit, not +/- sign, not #.
|
||||
result = mask.match(/[^\d\-\+#]/g);
|
||||
const decimal = (result && result[result.length - 1]) || '.'; // treat the right most symbol as decimal
|
||||
const group = (result && result[1] && result[0]) || ','; // treat the left most symbol as group separator
|
||||
|
||||
// split the decimal for the format string if any.
|
||||
mask = mask.split(decimal);
|
||||
// Fix the decimal first, toFixed will auto fill trailing zero.
|
||||
value = value.toFixed(mask[1] && mask[1].length);
|
||||
value = String(Number(value)); // convert number to string to trim off *all* trailing decimal zero(es)
|
||||
|
||||
// fill back any trailing zero according to format
|
||||
const posTrailZero = mask[1] && mask[1].lastIndexOf('0'); // look for last zero in format
|
||||
const part = value.split('.');
|
||||
// integer will get !part[1]
|
||||
if (!part[1] || (part[1] && part[1].length <= posTrailZero)) {
|
||||
value = (Number(value)).toFixed(posTrailZero + 1);
|
||||
}
|
||||
const szSep = mask[0].split(group); // look for separator
|
||||
mask[0] = szSep.join(''); // join back without separator for counting the pos of any leading 0.
|
||||
|
||||
const posLeadZero = mask[0] && mask[0].indexOf('0');
|
||||
if (posLeadZero > -1) {
|
||||
while (part[0].length < (mask[0].length - posLeadZero)) {
|
||||
part[0] = `0${part[0]}`;
|
||||
}
|
||||
} else if (Number(part[0]) === 0) {
|
||||
part[0] = '';
|
||||
}
|
||||
|
||||
value = value.split('.');
|
||||
value[0] = part[0];
|
||||
|
||||
// process the first group separator from decimal (.) only, the rest ignore.
|
||||
// get the length of the last slice of split result.
|
||||
const posSeparator = (szSep[1] && szSep[szSep.length - 1].length);
|
||||
if (posSeparator) {
|
||||
integer = value[0];
|
||||
str = '';
|
||||
offset = integer.length % posSeparator;
|
||||
len = integer.length;
|
||||
for (index = 0; index < len; index++) {
|
||||
str += integer.charAt(index); // ie6 only support charAt for sz.
|
||||
// -posSeparator so that won't trail separator on full length
|
||||
// jshint -W018
|
||||
if (!((index - offset + 1) % posSeparator) && index < len - posSeparator) {
|
||||
str += group;
|
||||
}
|
||||
}
|
||||
value[0] = str;
|
||||
}
|
||||
value[1] = (mask[1] && value[1]) ? decimal + value[1] : '';
|
||||
|
||||
// remove negative sign if result is zero
|
||||
result = value.join('');
|
||||
if (result === '0' || result === '') {
|
||||
// remove negative sign if result is zero
|
||||
isNegative = false;
|
||||
}
|
||||
|
||||
// put back any negation, combine integer and fraction, and add back prefix & suffix
|
||||
return prefix + ((isNegative ? '-' : '') + result) + suffix;
|
||||
}
|
||||
164
src/root.jsx
164
src/root.jsx
@@ -4,61 +4,129 @@ import HeadersTable from './headers-table/index.jsx';
|
||||
import DataTable from './data-table/index.jsx';
|
||||
import { LinkedScrollWrapper, LinkedScrollSection } from './linked-scroll';
|
||||
|
||||
const Root = ({ state, qlik, editmodeClass }) => (
|
||||
<div className="root">
|
||||
<LinkedScrollWrapper>
|
||||
<div className={`kpi-table ${editmodeClass}`}>
|
||||
<HeadersTable
|
||||
data={state.data}
|
||||
general={state.general}
|
||||
isKpi
|
||||
qlik={qlik}
|
||||
styling={state.styling}
|
||||
/>
|
||||
<LinkedScrollSection linkVertical>
|
||||
<DataTable
|
||||
data={state.data}
|
||||
general={state.general}
|
||||
qlik={qlik}
|
||||
renderData={false}
|
||||
styling={state.styling}
|
||||
/>
|
||||
</LinkedScrollSection>
|
||||
class Root extends React.PureComponent {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.onDataTableRefSet = this.onDataTableRefSet.bind(this);
|
||||
this.renderedTableWidth = 0;
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
const tableWidth = this.dataTableRef.getBoundingClientRect().width;
|
||||
if (this.renderedTableWidth !== tableWidth) {
|
||||
this.forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
onDataTableRefSet (element) {
|
||||
this.dataTableRef = element;
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { editmodeClass, component, state } = this.props;
|
||||
const { data, general, styling } = state;
|
||||
|
||||
// Determine cell- and column separator width
|
||||
let cellWidth = '0px';
|
||||
let columnSeparatorWidth = '';
|
||||
if (this.dataTableRef) {
|
||||
const tableWidth = this.dataTableRef.getBoundingClientRect().width;
|
||||
this.renderedTableWidth = tableWidth;
|
||||
|
||||
if (general.cellWidth) {
|
||||
cellWidth = general.cellWidth;
|
||||
if (general.useColumnSeparator) {
|
||||
columnSeparatorWidth = '8px';
|
||||
}
|
||||
} else {
|
||||
const headerMarginRight = 8;
|
||||
const borderWidth = 1;
|
||||
const rowCellCount = data.matrix[0].length;
|
||||
|
||||
let separatorCount = 0;
|
||||
let separatorWidth = 0;
|
||||
if (general.useColumnSeparator) {
|
||||
separatorCount = data.headers.dimension2.length - 1;
|
||||
separatorWidth = Math.min(Math.floor(tableWidth * 0.2 / separatorCount), 8);
|
||||
columnSeparatorWidth = `${separatorWidth}px`;
|
||||
}
|
||||
|
||||
const separatorWidthSum = (separatorWidth + borderWidth) * separatorCount;
|
||||
cellWidth = `${Math.floor((tableWidth - separatorWidthSum - headerMarginRight - borderWidth)
|
||||
/ rowCellCount) - borderWidth}px`;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="root">
|
||||
<LinkedScrollWrapper>
|
||||
<div className={`kpi-table ${editmodeClass}`}>
|
||||
<HeadersTable
|
||||
cellWidth={cellWidth}
|
||||
columnSeparatorWidth={columnSeparatorWidth}
|
||||
component={component}
|
||||
data={data}
|
||||
general={general}
|
||||
isKpi
|
||||
styling={styling}
|
||||
/>
|
||||
<LinkedScrollSection linkVertical>
|
||||
<DataTable
|
||||
cellWidth={cellWidth}
|
||||
columnSeparatorWidth={columnSeparatorWidth}
|
||||
component={component}
|
||||
data={data}
|
||||
general={general}
|
||||
renderData={false}
|
||||
styling={styling}
|
||||
/>
|
||||
</LinkedScrollSection>
|
||||
</div>
|
||||
<div
|
||||
className={`data-table ${editmodeClass}`}
|
||||
style={{ width: general.cellWidth ? 'auto' : '100%' }}
|
||||
ref={this.onDataTableRefSet}
|
||||
>
|
||||
<LinkedScrollSection linkHorizontal>
|
||||
<HeadersTable
|
||||
cellWidth={cellWidth}
|
||||
columnSeparatorWidth={columnSeparatorWidth}
|
||||
component={component}
|
||||
data={data}
|
||||
general={general}
|
||||
isKpi={false}
|
||||
styling={styling}
|
||||
/>
|
||||
</LinkedScrollSection>
|
||||
<LinkedScrollSection
|
||||
linkHorizontal
|
||||
linkVertical
|
||||
>
|
||||
<DataTable
|
||||
cellWidth={cellWidth}
|
||||
columnSeparatorWidth={columnSeparatorWidth}
|
||||
component={component}
|
||||
data={data}
|
||||
general={general}
|
||||
styling={styling}
|
||||
/>
|
||||
</LinkedScrollSection>
|
||||
</div>
|
||||
</LinkedScrollWrapper>
|
||||
</div>
|
||||
<div className={`data-table ${editmodeClass}`}>
|
||||
<LinkedScrollSection linkHorizontal>
|
||||
<HeadersTable
|
||||
data={state.data}
|
||||
general={state.general}
|
||||
isKpi={false}
|
||||
qlik={qlik}
|
||||
styling={state.styling}
|
||||
/>
|
||||
</LinkedScrollSection>
|
||||
<LinkedScrollSection
|
||||
linkHorizontal
|
||||
linkVertical
|
||||
>
|
||||
<DataTable
|
||||
data={state.data}
|
||||
general={state.general}
|
||||
qlik={qlik}
|
||||
styling={state.styling}
|
||||
/>
|
||||
</LinkedScrollSection>
|
||||
</div>
|
||||
</LinkedScrollWrapper>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Root.propTypes = {
|
||||
qlik: PropTypes.shape({}).isRequired,
|
||||
component: PropTypes.shape({}).isRequired,
|
||||
editmodeClass: PropTypes.string.isRequired,
|
||||
state: PropTypes.shape({
|
||||
data: PropTypes.object.isRequired,
|
||||
general: PropTypes.object.isRequired,
|
||||
styling: PropTypes.object.isRequired
|
||||
}).isRequired,
|
||||
editmodeClass: PropTypes.string.isRequired
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
export default Root;
|
||||
|
||||
@@ -9,21 +9,6 @@ export function distinctArray (array) {
|
||||
.map(entry => JSON.parse(entry));
|
||||
}
|
||||
|
||||
export function addSeparators (number, thousandSeparator, decimalSeparator, numberOfDecimals) {
|
||||
const numberString = number.toFixed(numberOfDecimals);
|
||||
const numberStringParts = numberString.split('.');
|
||||
let [
|
||||
wholeNumber,
|
||||
decimal
|
||||
] = numberStringParts;
|
||||
decimal = numberStringParts.length > 1 ? decimalSeparator + decimal : '';
|
||||
const regexCheckForThousand = /(\d+)(\d{3})/;
|
||||
while (regexCheckForThousand.test(wholeNumber)) {
|
||||
wholeNumber = wholeNumber.replace(regexCheckForThousand, `$1${thousandSeparator}$2`);
|
||||
}
|
||||
return wholeNumber + decimal;
|
||||
}
|
||||
|
||||
export function Deferred () {
|
||||
this.promise = new Promise((resolve, reject) => {
|
||||
this.resolve = resolve;
|
||||
@@ -31,7 +16,7 @@ export function Deferred () {
|
||||
});
|
||||
}
|
||||
|
||||
export function injectSeparators (array, shouldHaveSeparator, suppliedOptions) {
|
||||
export function injectSeparators (array, columnSeparatorWidth, suppliedOptions) {
|
||||
const defaultOptions = {
|
||||
atEvery: 1,
|
||||
separator: { isSeparator: true }
|
||||
@@ -41,7 +26,7 @@ export function injectSeparators (array, shouldHaveSeparator, suppliedOptions) {
|
||||
...suppliedOptions
|
||||
};
|
||||
|
||||
if (!shouldHaveSeparator) {
|
||||
if (!columnSeparatorWidth) {
|
||||
return array;
|
||||
}
|
||||
return array.reduce((result, entry, index) => {
|
||||
|
||||
Reference in New Issue
Block a user