Compare commits

...

31 Commits

Author SHA1 Message Date
Albert Backenhof
79339a8963 Merge pull request #70 from qlik-oss/QLIK-95962/SpanWidth
Add option to fit cells to chart width
2019-05-29 12:19:59 +02:00
Albert Backenhof
a6faeeb656 Add option to fit cells to chart width
-To achieve pixel perfection the container of both
 header and data needs to calculate the size of the
 child tables' cells.

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

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

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

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

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

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

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

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

Issue: QLIK-95019
2019-04-30 13:04:42 +02:00
19 changed files with 548 additions and 742 deletions

View File

@@ -1,52 +1,22 @@
# P&L Smart Pivot, a Qlik Sense Extension for Financial reporting
[![CircleCI](https://circleci.com/gh/qlik-oss/PLSmartPivot.svg?style=svg)](https://circleci.com/gh/qlik-oss/PLSmartPivot)
This extension is part of the extension bundles for Qlik Sense. The repository is maintained and moderated by Qlik RD.
This extension is useful to create reports where the look&feel is rellevantand and pivot a second dimension is needed. Based on P&L Smart.
It's specifically focused on financial reports, trying to solve some common needs of this area:
- smart export to excel
- easy creation of reports
- custom corporate reporting (bold, italic, background color, letter size, headers,...)
- selections inside the reports
- custom external templates
- analytical reports
# Manual
You'll find a manual [Qlik Sense P&LSmart Pivot Extension Manual.pdf](resources/Qlik Sense P&LSmart Pivot Extension Manual.pdf) and one app example [P&LSmartPivot_demo.qvf](resources/P&LSmartPivot_demo.qvf).
# Installation
1. Download the extension zip, `qlik-smart-pivot_<version>.zip`, from the latest release(https://github.com/qlik-oss/PLSmartPivot/releases/latest)
2. Install the extension:
a. **Qlik Sense Desktop**: unzip to a directory under [My Documents]/Qlik/Sense/Extensions.
b. **Qlik Sense Server**: import the zip file in the QMC.
Feel free to fork and suggest pull requests for improvements and bug fixes. Changes will be moderated and reviewed before inclusion in future bundle versions. Please note that emphasis is on backward compatibility, i.e. breaking changes will most likely not be approved.
Usage documentation for the extension is available at https://help.qlik.com.
# Developing the extension
If you want to do code changes to the extension follow these simple steps to get going.
1. Get Qlik Sense Desktop
1. Create a new app and add the extension to a sheet.
1. Create a new app and add P&L pivot to a sheet.
2. Clone the repository
3. Run `npm install`
4. Set the environment variable `BUILD_PATH` to your extensions directory. It will be something like `C:/Users/<user>/Documents/Qlik/Sense/Extensions/<extension_name>`.
5. You now have two options. Either run the watch task or the build task. They are explained below. Both of them default to development mode but can be run in production by setting `NODE_ENV=production` before running the npm task.
a. **Watch**: `npm run watch`. This will start a watcher which will rebuild the extension and output all needed files to the `buildFolder` for each code change you make. See your changes directly in your Qlik Sense app.
b. **Build**: `npm run build`. If you want to build the extension package. The output zip-file can be found in the `buildFolder`.
4. Run `npm run build` - to build a dev-version to the /dist folder.
5. Move the content of the /dist folder to the extension directory. Usually in `C:/Users/<user>/Documents/Qlik/Sense/Extensions/qlik-smart-pivot`.
# Original authors
[github.com/iviasensio](https://github.com/iviasensio)
# License
Released under the [MIT License](LICENSE).
Released under the [MIT License](LICENSE).

View File

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

View File

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

View File

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

View File

@@ -22,25 +22,33 @@ 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) {
const app = qlik.currApp(component);
let properties;
if (component.backendApi.isSnapshot) {
// Fetch properties of source
properties = (await app.getObjectProperties(layout.sourceObjectId)).properties;
} else {
properties = await component.backendApi.getProperties();
return layout.snapshotData.dataCube;
}
return buildDataCube(
properties.qHyperCubeDef, layout.qHyperCube.qDimensionInfo.length === 2, app);
const app = qlik.currApp(component);
const properties = (await component.backendApi.getProperties());
// If this is a master object, fetch the hyperCubeDef of the original object
const hyperCubeDef = properties.qExtendsId
? (await app.getObjectProperties(properties.qExtendsId)).properties.qHyperCubeDef
: properties.qHyperCubeDef;
hyperCubeDef.qStateName = layout.qStateName || "";
return buildDataCube(hyperCubeDef, layout.qHyperCube.qDimensionInfo.length === 2, app);
}
export function initializeDesignList (component, layout) {
if (component.backendApi.isSnapshot) {
return layout.snapshotData.designList;
}
if (!layout.stylingfield) {
return null;
}

View File

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

View File

@@ -1,19 +1,31 @@
function removeAllTooltips (node) {
const tooltips = node.querySelectorAll('.tooltip');
[].forEach.call(tooltips, tooltip => {
if (tooltip.parentNode) {
tooltip.parentNode.removeChild(tooltip);
function cleanupNodes (node) {
const removables = node.querySelectorAll('.tooltip,input');
[].forEach.call(removables, removeable => {
if (removeable.parentNode) {
removeable.parentNode.removeChild(removeable);
}
});
}
function buildTableHTML (title, subtitle, footnote) {
function buildTableHTML (containerElement, title, subtitle, footnote) {
const titleHTML = `<p style="font-size:15pt"><b>${title}</b></p>`;
const subtitleHTML = `<p style="font-size:11pt">${subtitle}</p>`;
const footnoteHTML = `<p style="font-size:11pt"><i>Note:</i>${footnote}</p>`;
const dataTableClone = document.querySelector('.data-table').cloneNode(true);
const footnoteHTML = `<p style="font-size:11pt">${footnote}</p>`;
const kpiTableClone = containerElement[0].querySelector('.kpi-table').cloneNode(true);
const dataTableClone = containerElement[0].querySelector('.data-table').cloneNode(true);
cleanupNodes(kpiTableClone);
cleanupNodes(kpiTableClone);
removeAllTooltips(dataTableClone);
const kpiTableBodies = kpiTableClone.querySelectorAll('tbody');
const dataTableBodies = dataTableClone.querySelectorAll('tbody');
const kpiHeader = kpiTableBodies[0].querySelector('tr');
const dataTableHeaders = dataTableBodies[0].querySelectorAll('tr');
const kpiRows = kpiTableBodies[1].querySelectorAll('tr');
const dataRows = dataTableBodies[1].querySelectorAll('tr');
let combinedRows = '';
for (let i = 0; i < kpiRows.length; i++) {
combinedRows += `<tr>${kpiRows[i].innerHTML}${dataRows[i].innerHTML}</tr>`;
}
const tableHTML = `
<html
@@ -41,8 +53,23 @@ function buildTableHTML (title, subtitle, footnote) {
<body>
${titleHTML.length > 0 ? titleHTML : ''}
${subtitleHTML.length > 0 ? subtitleHTML : ''}
<div>
<table>
<tbody>
<tr>
${kpiHeader.innerHTML}
${dataTableHeaders[0].innerHTML}
</tr>
${dataTableHeaders.length > 1 ? dataTableHeaders[1].outerHTML : ''}
</tbody>
</table>
<table>
<tbody>
${combinedRows}
</tbody>
</table>
</div>
${footnoteHTML.length > 0 ? footnoteHTML : ''}
${dataTableClone.outerHTML}
</body>
</html>
`.split('>.<')
@@ -55,15 +82,15 @@ function buildTableHTML (title, subtitle, footnote) {
function downloadXLS (html) {
const filename = 'analysis.xls';
const blobObject = new Blob([html], { type: 'application/vnd.ms-excel' });
// IE/Edge
if (window.navigator.msSaveOrOpenBlob) {
const blobObject = new Blob([html]);
return window.navigator.msSaveOrOpenBlob(blobObject, filename);
}
const dataURI = generateDataURI(html);
const link = window.document.createElement('a');
link.href = dataURI;
link.href = URL.createObjectURL(blobObject);
link.download = filename;
document.body.appendChild(link);
link.click();
@@ -72,15 +99,8 @@ function downloadXLS (html) {
return true;
}
function generateDataURI (html) {
const dataType = 'data:application/vnd.ms-excel;base64,';
const data = window.btoa(unescape(encodeURIComponent(html)));
return `${dataType}${data}`;
}
export function exportXLS (title, subtitle, footnote) {
export function exportXLS (containerElement, title, subtitle, footnote) {
// original was removing icon when starting export, disable and some spinner instead, shouldn't take enough time to warrant either..?
const table = buildTableHTML(title, subtitle, footnote);
const table = buildTableHTML(containerElement, title, subtitle, footnote);
downloadXLS(table);
}

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import ExportButton from '../export-button.jsx';
import { HEADER_FONT_SIZE } from '../initialize-transformed';
const ExportColumnHeader = ({ baseCSS, general, title, allowExcelExport, hasSecondDimension, styling }) => {
const ExportColumnHeader = ({ component, baseCSS, general, title, allowExcelExport, hasSecondDimension, styling }) => {
const rowSpan = hasSecondDimension ? 2 : 1;
const isMediumFontSize = styling.headerOptions.fontSizeAdjustment === HEADER_FONT_SIZE.MEDIUM;
const style = {
@@ -22,6 +22,7 @@ const ExportColumnHeader = ({ baseCSS, general, title, allowExcelExport, hasSeco
style={style}
>
<ExportButton
component={component}
excelExport={allowExcelExport}
general={general}
/>
@@ -31,6 +32,7 @@ const ExportColumnHeader = ({ baseCSS, general, title, allowExcelExport, hasSeco
};
ExportColumnHeader.propTypes = {
component: PropTypes.shape({}).isRequired,
allowExcelExport: PropTypes.bool.isRequired,
baseCSS: PropTypes.shape({}).isRequired,
general: PropTypes.shape({}).isRequired,

View File

@@ -5,137 +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
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({})

View File

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

View File

@@ -1,5 +1,9 @@
import paint from './paint.jsx';
import definition from './definition';
import { initializeDataCube, initializeDesignList } from './dataset';
import initializeStore from './store';
import React from 'react';
import ReactDOM from 'react-dom';
import Root from './root.jsx';
import './main.less';
if (!window._babelPolyfill) { // eslint-disable-line no-underscore-dangle
@@ -53,16 +57,34 @@ export default {
exportData: true,
snapshot: true
},
paint ($element, layout) {
try {
paint($element, layout, this);
} catch (exception) {
console.error(exception); // eslint-disable-line no-console
throw exception;
}
paint: async function ($element, layout) {
const dataCube = await initializeDataCube(this, layout);
const designList = await initializeDesignList(this, layout);
const state = await initializeStore({
$element,
component: this,
dataCube,
designList,
layout
});
const editmodeClass = this.inAnalysisState() ? '' : 'edit-mode';
const jsx = (
<Root
editmodeClass={editmodeClass}
component={this}
state={state}
/>
);
ReactDOM.render(jsx, $element[0]);
},
snapshot: {
canTakeSnapshot: true
},
setSnapshotData: async function (snapshotLayout) {
snapshotLayout.snapshotData.dataCube = await initializeDataCube(this, snapshotLayout);
snapshotLayout.snapshotData.designList = await initializeDesignList(this, snapshotLayout);
return snapshotLayout;
},
version: 1.0
};

View File

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

View File

@@ -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;
}
@@ -37,6 +35,10 @@
width: auto;
}
tr {
height: 25px;
}
td,
th {
padding: 5px !important; // prevent overwriting from single object
@@ -55,10 +57,8 @@
}
.empty {
width: 3%;
background: #fff;
min-width: 4px !important;
max-width: 4px !important;
padding: 0 !important;
}
th.main-kpi {
@@ -70,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;
@@ -145,14 +84,6 @@
color: #fff;
}
.grid-cells-header {
padding: 0;
}
.grid-cells-title {
min-width: 522px;
}
.grid {
height: 50px;
width: 350px;

View File

@@ -1,132 +0,0 @@
import { addSeparators } from './utilities';
export function ApplyPreMask (mask, value) { // aqui
if (mask.indexOf(';') >= 0) {
if (value >= 0) {
switch (mask.substring(0, mask.indexOf(';'))) {
case '#.##0':
return (addSeparators(value, '.', ',', 0));
case '#,##0':
return (addSeparators(value, ',', '.', 0));
case '+#.##0':
return (addSeparators(value, '.', ',', 0));
case '+#,##0':
return (addSeparators(value, ',', '.', 0));
default:
return (applyMask(mask.substring(0, mask.indexOf(';')), value));
}
} else {
const vMyValue = value * -1;
let vMyMask = mask.substring(mask.indexOf(';') + 1, mask.length);
vMyMask = vMyMask.replace('(', '');
vMyMask = vMyMask.replace(')', '');
switch (vMyMask) {
case '#.##0':
return (`(${addSeparators(vMyValue, '.', ',', 0)})`);
case '#,##0':
return (`(${addSeparators(vMyValue, ',', '.', 0)})`);
case '-#.##0':
return (`(${addSeparators(vMyValue, '.', ',', 0)})`);
case '-#,##0':
return (`(${addSeparators(vMyValue, ',', '.', 0)})`);
default:
return (`(${applyMask(vMyMask, vMyValue)})`);
}
}
} else {
return (applyMask(mask, value));
}
}
function applyMask (originalMask, originalValue) {
if (!originalMask || isNaN(Number(originalValue))) {
return originalValue;
}
let isNegative;
let result;
let integer;
// find prefix/suffix
let len = originalMask.length;
const start = originalMask.search(/[0-9\-\+#]/);
const prefix = start > 0 ? originalMask.substring(0, start) : '';
// reverse string: not an ideal method if there are surrogate pairs
let str = originalMask.split('')
.reverse()
.join('');
const end = str.search(/[0-9\-\+#]/);
let offset = len - end;
const substr = originalMask.substring(offset, offset + 1);
let index = offset + ((substr === '.' || (substr === ',')) ? 1 : 0);
const suffix = end > 0 ? originalMask.substring(index, len) : '';
// mask with prefix & suffix removed
let mask = originalMask.substring(start, index);
// convert any string to number according to formation sign.
let value = mask.charAt(0) === '-' ? -originalValue : Number(originalValue);
isNegative = value < 0 ? value = -value : 0; // process only abs(), and turn on flag.
// search for separator for grp & decimal, anything not digit, not +/- sign, not #.
result = mask.match(/[^\d\-\+#]/g);
const decimal = (result && result[result.length - 1]) || '.'; // treat the right most symbol as decimal
const group = (result && result[1] && result[0]) || ','; // treat the left most symbol as group separator
// split the decimal for the format string if any.
mask = mask.split(decimal);
// Fix the decimal first, toFixed will auto fill trailing zero.
value = value.toFixed(mask[1] && mask[1].length);
value = String(Number(value)); // convert number to string to trim off *all* trailing decimal zero(es)
// fill back any trailing zero according to format
const posTrailZero = mask[1] && mask[1].lastIndexOf('0'); // look for last zero in format
const part = value.split('.');
// integer will get !part[1]
if (!part[1] || (part[1] && part[1].length <= posTrailZero)) {
value = (Number(value)).toFixed(posTrailZero + 1);
}
const szSep = mask[0].split(group); // look for separator
mask[0] = szSep.join(''); // join back without separator for counting the pos of any leading 0.
const posLeadZero = mask[0] && mask[0].indexOf('0');
if (posLeadZero > -1) {
while (part[0].length < (mask[0].length - posLeadZero)) {
part[0] = `0${part[0]}`;
}
} else if (Number(part[0]) === 0) {
part[0] = '';
}
value = value.split('.');
value[0] = part[0];
// process the first group separator from decimal (.) only, the rest ignore.
// get the length of the last slice of split result.
const posSeparator = (szSep[1] && szSep[szSep.length - 1].length);
if (posSeparator) {
integer = value[0];
str = '';
offset = integer.length % posSeparator;
len = integer.length;
for (index = 0; index < len; index++) {
str += integer.charAt(index); // ie6 only support charAt for sz.
// -posSeparator so that won't trail separator on full length
// jshint -W018
if (!((index - offset + 1) % posSeparator) && index < len - posSeparator) {
str += group;
}
}
value[0] = str;
}
value[1] = (mask[1] && value[1]) ? decimal + value[1] : '';
// remove negative sign if result is zero
result = value.join('');
if (result === '0' || result === '') {
// remove negative sign if result is zero
isNegative = false;
}
// put back any negation, combine integer and fraction, and add back prefix & suffix
return prefix + ((isNegative ? '-' : '') + result) + suffix;
}

View File

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

View File

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

View File

@@ -9,21 +9,6 @@ export function distinctArray (array) {
.map(entry => JSON.parse(entry));
}
export function addSeparators (number, thousandSeparator, decimalSeparator, numberOfDecimals) {
const numberString = number.toFixed(numberOfDecimals);
const numberStringParts = numberString.split('.');
let [
wholeNumber,
decimal
] = numberStringParts;
decimal = numberStringParts.length > 1 ? decimalSeparator + decimal : '';
const regexCheckForThousand = /(\d+)(\d{3})/;
while (regexCheckForThousand.test(wholeNumber)) {
wholeNumber = wholeNumber.replace(regexCheckForThousand, `$1${thousandSeparator}$2`);
}
return wholeNumber + decimal;
}
export function Deferred () {
this.promise = new Promise((resolve, reject) => {
this.resolve = resolve;
@@ -31,7 +16,7 @@ export function Deferred () {
});
}
export function injectSeparators (array, shouldHaveSeparator, suppliedOptions) {
export function injectSeparators (array, columnSeparatorWidth, suppliedOptions) {
const defaultOptions = {
atEvery: 1,
separator: { isSeparator: true }
@@ -41,7 +26,7 @@ export function injectSeparators (array, shouldHaveSeparator, suppliedOptions) {
...suppliedOptions
};
if (!shouldHaveSeparator) {
if (!columnSeparatorWidth) {
return array;
}
return array.reduce((result, entry, index) => {