Compare commits

...

14 Commits

Author SHA1 Message Date
giovanni hanselius
44b33b4c92 Merge pull request #16 from qlik-oss/feature/QPE-550
[QPE-550] fix excel export
2019-02-21 16:48:34 +01:00
giovanni hanselius
61b339b146 Merge pull request #18 from qlik-oss/QPE-479
[QPE-479] Qlik defaults
2019-02-21 15:42:31 +01:00
giovanni hanselius
fc363d7739 Merge pull request #20 from qlik-oss/export
exporting image and PDF enabled
2019-02-20 16:01:27 +01:00
ahmed-Bazzara
35d4dde118 exporting image and PDF enabled 2019-02-20 15:50:54 +01:00
ahmed-Bazzara
e70e76a401 small font size is set to be default
and its value matched to Qlik defalut font size
2019-02-20 15:44:36 +01:00
ahmed-Bazzara
34477d7ef1 qlik font added to the fonts dropdown
and was made a default value
2019-02-18 16:12:40 +01:00
Tobias Åström
b65d1c51fc Update qlik-smart-pivot.qext 2019-02-16 10:49:46 +01:00
Tobias Åström
9111ec762b Update config.yml 2019-02-16 10:48:49 +01:00
Kristoffer Lind
b86806d4cd cleanup tooltips (resulted in whatever header was last hovered to be appended to each column header in xls) 2019-02-14 15:14:23 +01:00
Kristoffer Lind
c3651a37da Merge branch 'master' into feature/QPE-550 2019-02-14 12:36:55 +01:00
Kristoffer Lind
8b843e028a fix excel export 2019-02-14 11:08:34 +01:00
John Lunde
2bdd98aaca Merge pull request #7 from qlik-oss/feature/QPE-563
[QPE-563] Let react handle rendering
2019-02-14 10:38:32 +01:00
Kristoffer Lind
d723451656 revert accidental design change 2019-02-13 13:52:20 +01:00
Kristoffer Lind
f2f201c6e2 let react handle rendering 2019-02-12 11:26:31 +01:00
15 changed files with 448 additions and 501 deletions

View File

@@ -18,6 +18,14 @@ jobs:
- run:
name: Install dependencies
command: npm install
- run:
name: BlackDuck scan
command: curl -s https://blackducksoftware.github.io/hub-detect/hub-detect.sh | bash -s -- \
--blackduck.url="https://qliktech.blackducksoftware.com" \
--blackduck.trust.cert=true \
--blackduck.username="svc-blackduck" \
--blackduck.password=${svc_blackduck} \
--detect.project.name="viz-bundle-qlik-smart-pivot"
- run:
name: Run tests
command: npm run test-once

View File

@@ -1,6 +1,6 @@
{
"name": "Smart pivot",
"description": "Formatted table for P&L reports.",
"name": "P&L pivot",
"description": "Profit & Loss reporting with color and font customizations.",
"type": "visualization",
"version": "X.Y.Z",
"icon": "table",

View File

@@ -5,7 +5,7 @@ import { addSeparators } from '../utilities';
function formatMeasurementValue (measurement, styling) {
// TODO: measurement.name is a horrible propertyname, it's actually the column header
const isColumnPercentageBased = measurement.name.substring(0, 1) === '%';
const isColumnPercentageBased = measurement.parents.measurement.header.substring(0, 1) === '%';
let formattedMeasurementValue = '';
if (isColumnPercentageBased) {
if (isNaN(measurement.value)) {
@@ -62,50 +62,74 @@ function getSemaphoreColors (measurement, semaphoreColors) {
return semaphoreColors.statusColors.normal;
}
const DataCell = ({ data, general, measurement, styleBuilder, styling }) => {
const isColumnPercentageBased = measurement.name.substring(0, 1) === '%';
let formattedMeasurementValue = formatMeasurementValue(measurement, styling);
if (styleBuilder.hasComments()) {
formattedMeasurementValue = '.';
class DataCell extends React.PureComponent {
constructor (props) {
super(props);
this.handleSelect = this.handleSelect.bind(this);
}
let cellStyle = {
fontFamily: styling.options.fontFamily,
...styleBuilder.getStyle(),
paddingRight: '4px',
textAlign: 'right'
handleSelect () {
const { data: { meta: { dimensionCount } }, general: { allowFilteringByClick }, measurement, qlik } = this.props;
const hasSecondDimension = dimensionCount > 1;
if (!allowFilteringByClick) {
return;
}
};
const { semaphoreColors } = styling;
const isValidSemaphoreValue = !styleBuilder.hasComments() && !isNaN(measurement.value);
const shouldHaveSemaphoreColors = semaphoreColors.fieldsToApplyTo.applyToAll || semaphoreColors.fieldsToApplyTo.specificFields.indexOf(measurement.name) !== -1;
if (isValidSemaphoreValue && shouldHaveSemaphoreColors) {
const { backgroundColor, color } = getSemaphoreColors(measurement, semaphoreColors);
cellStyle = {
backgroundColor,
color,
qlik.backendApi.selectValues(0, [measurement.parents.dimension1.elementNumber], true);
if (hasSecondDimension) {
qlik.backendApi.selectValues(1, [measurement.parents.dimension2.elementNumber], true);
}
}
render () {
const { data, general, measurement, styleBuilder, styling } = this.props;
const isColumnPercentageBased = measurement.name.substring(0, 1) === '%';
let formattedMeasurementValue = formatMeasurementValue(measurement, styling);
if (styleBuilder.hasComments()) {
formattedMeasurementValue = '.';
}
let cellStyle = {
fontFamily: styling.options.fontFamily,
fontSize: styleBuilder.getStyle().fontSize,
...styleBuilder.getStyle(),
paddingLeft: '4px',
textAlign: 'right'
};
}
const { semaphoreColors } = styling;
const isValidSemaphoreValue = !styleBuilder.hasComments() && !isNaN(measurement.value);
const shouldHaveSemaphoreColors = semaphoreColors.fieldsToApplyTo.applyToAll || semaphoreColors.fieldsToApplyTo.specificFields.indexOf(measurement.parents.dimension1.header) !== -1;
if (isValidSemaphoreValue && shouldHaveSemaphoreColors) {
const { backgroundColor, color } = getSemaphoreColors(measurement, semaphoreColors);
cellStyle = {
backgroundColor,
color,
fontFamily: styling.options.fontFamily,
fontSize: styleBuilder.getStyle().fontSize,
paddingLeft: '4px',
textAlign: 'right'
};
}
let cellClass = 'grid-cells';
const shouldUseSmallCells = isColumnPercentageBased && data.headers.measurements.length > 1;
if (shouldUseSmallCells) {
cellClass = 'grid-cells-small';
}
let cellClass = 'grid-cells';
const shouldUseSmallCells = isColumnPercentageBased && data.headers.measurements.length > 1;
if (shouldUseSmallCells) {
cellClass = 'grid-cells-small';
}
return (
<td
className={`${cellClass}${general.cellSuffix}`}
style={cellStyle}
>
{formattedMeasurementValue}
</td>
);
};
return (
<td
className={`${cellClass}${general.cellSuffix}`}
onClick={this.handleSelect}
style={cellStyle}
>
{formattedMeasurementValue}
</td>
);
}
}
DataCell.propTypes = {
data: PropTypes.shape({
@@ -121,6 +145,11 @@ DataCell.propTypes = {
name: PropTypes.string,
value: PropTypes.any
}).isRequired,
qlik: PropTypes.shape({
backendApi: PropTypes.shape({
selectValues: PropTypes.func.isRequired
}).isRequired
}).isRequired,
styleBuilder: PropTypes.shape({
hasComments: PropTypes.func.isRequired
}).isRequired,

View File

@@ -3,9 +3,10 @@ import PropTypes from 'prop-types';
import StyleBuilder from '../style-builder';
import DataCell from './data-cell.jsx';
import HeaderPadding from './header-padding.jsx';
import RowHeader from './row-header.jsx';
import { injectSeparators } from '../utilities';
const DataTable = ({ data, general, styling }) => {
const DataTable = ({ data, general, qlik, renderData, styling }) => {
const {
headers: {
dimension1,
@@ -17,79 +18,85 @@ const DataTable = ({ data, general, styling }) => {
return (
<div className="row-wrapper">
<table>
{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()
};
<tbody>
{dimension1.map((dimensionEntry, dimensionIndex) => {
const rowHeaderText = dimensionEntry.displayValue || '';
if (rowHeaderText === '-') {
return null;
}
const styleBuilder = new StyleBuilder(styling);
if (styling.hasCustomFileStyle) {
styleBuilder.parseCustomFileStyle(rowHeaderText);
} else {
styleBuilder.applyStandardAttributes(dimensionIndex);
styleBuilder.applyCustomStyle({
fontSize: `${14 + styling.options.fontSizeAdjustment}px`
});
}
const rowStyle = {
fontFamily: styling.options.fontFamily,
width: '230px',
...styleBuilder.getStyle()
};
return (
<tr key={dimensionEntry.displayValue}>
<td
className="fdim-cells"
style={rowStyle}
>
<HeaderPadding
return (
<tr key={dimensionEntry.displayValue}>
<RowHeader
entry={dimensionEntry}
qlik={qlik}
rowStyle={rowStyle}
styleBuilder={styleBuilder}
styling={styling}
/>
{dimensionEntry.displayValue}
</td>
{injectSeparators(
matrix[dimensionIndex],
styling.useSeparatorColumns,
{ atEvery: measurements.length }
).map(measurementData => {
if (measurementData.isSeparator) {
const separatorStyle = {
color: 'white',
fontFamily: styling.options.fontFamily,
fontSize: `${12 + styling.options.fontSizeAdjustment}px`
};
{renderData && injectSeparators(
matrix[dimensionIndex],
styling.useSeparatorColumns,
{ atEvery: measurements.length }
).map((measurementData, index) => {
if (measurementData.isSeparator) {
const separatorStyle = {
color: 'white',
fontFamily: styling.options.fontFamily,
fontSize: `${12 + styling.options.fontSizeAdjustment}px`
};
return (
<td
className="empty"
key={`${dimensionEntry.displayValue}-${index}-separator`}
style={separatorStyle}
>
*
</td>
);
}
const { dimension1: dimension1Info, dimension2, measurement } = measurementData.parents;
const id = `${dimension1Info.elementNumber}-${dimension2 && dimension2.elementNumber}-${measurement.header}`;
return (
<td
className="empty"
key={`${dimensionEntry.displayValue}-${measurementData.name}-separator`}
style={separatorStyle}
>
*
</td>
<DataCell
data={data}
general={general}
key={`${dimensionEntry.displayValue}-${id}`}
measurement={measurementData}
qlik={qlik}
styleBuilder={styleBuilder}
styling={styling}
/>
);
}
return (
<DataCell
data={data}
general={general}
key={`${dimensionEntry.displayValue}-${measurementData.name}`}
measurement={measurementData}
styleBuilder={styleBuilder}
styling={styling}
/>
);
})}
</tr>
);
})}
})}
</tr>
);
})}
</tbody>
</table>
</div>
);
};
DataTable.defaultProps = {
renderData: true
};
DataTable.propTypes = {
data: PropTypes.shape({
headers: PropTypes.shape({
@@ -98,6 +105,8 @@ DataTable.propTypes = {
matrix: PropTypes.arrayOf(PropTypes.array.isRequired).isRequired
}).isRequired,
general: PropTypes.shape({}).isRequired,
qlik: PropTypes.shape({}).isRequired,
renderData: PropTypes.bool,
styling: PropTypes.shape({
hasCustomFileStyle: PropTypes.bool.isRequired
}).isRequired

View File

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

View File

@@ -139,6 +139,10 @@ const formatted = {
component: 'dropdown',
label: 'FontFamily',
options: [
{
value: 'QlikView Sans',
label: 'QlikView Sans'
},
{
value: 'Arial',
label: 'Arial'
@@ -164,7 +168,7 @@ const formatted = {
label: 'Verdana'
}
],
defaultValue: 'Calibri'
defaultValue: 'QlikView Sans'
},
DataFontSize: {
ref: 'lettersize',
@@ -181,7 +185,7 @@ const formatted = {
label: 'Medium'
}
],
defaultValue: 2
defaultValue: 1
},
ColumnWidthSlider: {
type: 'number',

View File

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

View File

@@ -1,13 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
import { exportXLS } from './excel-export';
// TODO: move interaction logic in here from excel-export.js
class ExportButton extends React.PureComponent {
constructor (props) {
super(props);
this.handleExport = this.handleExport.bind(this);
}
handleExport () {
const { excelExport, general } = this.props;
const { title, subtitle, footnote } = general;
if (excelExport) {
exportXLS(title, subtitle, footnote);
}
}
render () {
const { excelExport } = this.props;
return excelExport === true && (
<input
className="icon-xls"
onClick={this.handleExport}
src="/Extensions/qlik-smart-pivot/Excel.png"
type="image"
/>
@@ -20,7 +34,8 @@ ExportButton.defaultProps = {
};
ExportButton.propTypes = {
excelExport: PropTypes.bool
excelExport: PropTypes.bool,
general: PropTypes.shape({}).isRequired
};
export default ExportButton;

View File

@@ -1,24 +1,39 @@
import React from 'react';
import PropTypes from 'prop-types';
const ColumnHeader = ({ baseCSS, cellSuffix, colSpan, styling, title }) => {
const style = {
...baseCSS,
fontSize: `${14 + styling.headerOptions.fontSizeAdjustment} px`,
height: '45px',
verticalAlign: 'middle'
};
class ColumnHeader extends React.PureComponent {
constructor (props) {
super(props);
return (
<th
className={`grid-cells2${cellSuffix}`}
colSpan={colSpan}
style={style}
>
{title}
</th>
);
};
this.handleSelect = this.handleSelect.bind(this);
}
handleSelect () {
const { entry, qlik } = this.props;
qlik.backendApi.selectValues(1, [entry.elementNumber], true);
}
render () {
const { baseCSS, cellSuffix, colSpan, entry, styling } = this.props;
const style = {
...baseCSS,
fontSize: `${14 + styling.headerOptions.fontSizeAdjustment} px`,
height: '45px',
verticalAlign: 'middle'
};
return (
<th
className={`grid-cells2${cellSuffix}`}
colSpan={colSpan}
onClick={this.handleSelect}
style={style}
>
{entry.displayValue}
</th>
);
}
}
ColumnHeader.defaultProps = {
cellSuffix: '',
@@ -29,12 +44,20 @@ ColumnHeader.propTypes = {
baseCSS: PropTypes.shape({}).isRequired,
cellSuffix: PropTypes.string,
colSpan: PropTypes.number,
entry: PropTypes.shape({
elementNumber: PropTypes.number.isRequired,
name: PropTypes.string.isRequired
}).isRequired,
qlik: PropTypes.shape({
backendApi: PropTypes.shape({
selectValues: PropTypes.func.isRequired
}).isRequired
}).isRequired,
styling: PropTypes.shape({
headerOptions: PropTypes.shape({
fontSizeAdjustment: PropTypes.number.isRequired
}).isRequired
}).isRequired,
title: PropTypes.string.isRequired
}).isRequired
};
export default ColumnHeader;

View File

@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import ExportButton from '../export-button.jsx';
const ExportColumnHeader = ({ baseCSS, title, allowExcelExport, hasSecondDimension, styling }) => {
const ExportColumnHeader = ({ baseCSS, general, title, allowExcelExport, hasSecondDimension, styling }) => {
const rowSpan = hasSecondDimension ? 2 : 1;
const style = {
...baseCSS,
@@ -19,7 +19,10 @@ const ExportColumnHeader = ({ baseCSS, title, allowExcelExport, hasSecondDimensi
rowSpan={rowSpan}
style={style}
>
<ExportButton excelExport={allowExcelExport} />
<ExportButton
excelExport={allowExcelExport}
general={general}
/>
{title}
</th>
);
@@ -28,6 +31,7 @@ const ExportColumnHeader = ({ baseCSS, title, allowExcelExport, hasSecondDimensi
ExportColumnHeader.propTypes = {
allowExcelExport: PropTypes.bool.isRequired,
baseCSS: PropTypes.shape({}).isRequired,
general: PropTypes.shape({}).isRequired,
hasSecondDimension: PropTypes.bool.isRequired,
styling: PropTypes.shape({
headerOptions: PropTypes.shape({

View File

@@ -5,7 +5,7 @@ import ColumnHeader from './column-header.jsx';
import MeasurementColumnHeader from './measurement-column-header.jsx';
import { injectSeparators } from '../utilities';
const HeadersTable = ({ data, general, styling }) => {
const HeadersTable = ({ data, general, qlik, styling }) => {
const baseCSS = {
backgroundColor: styling.headerOptions.colorSchema,
color: styling.headerOptions.textColor,
@@ -29,6 +29,7 @@ const HeadersTable = ({ data, general, styling }) => {
<ExportColumnHeader
allowExcelExport={general.allowExcelExport}
baseCSS={baseCSS}
general={general}
hasSecondDimension={hasSecondDimension}
styling={styling}
title={dimension1[0].name}
@@ -43,7 +44,7 @@ const HeadersTable = ({ data, general, styling }) => {
styling={styling}
/>
))}
{hasSecondDimension && injectSeparators(dimension2, styling.useSeparatorColumns).map(entry => {
{hasSecondDimension && injectSeparators(dimension2, styling.useSeparatorColumns).map((entry, index) => {
if (entry.isSeparator) {
const separatorStyle = {
color: 'white',
@@ -54,6 +55,7 @@ const HeadersTable = ({ data, general, styling }) => {
return (
<th
className="empty"
key={index}
style={separatorStyle}
>
*
@@ -65,16 +67,17 @@ const HeadersTable = ({ data, general, styling }) => {
baseCSS={baseCSS}
cellSuffix={general.cellSuffix}
colSpan={measurements.length}
entry={entry}
key={entry.displayValue}
qlik={qlik}
styling={styling}
title={entry.displayValue}
/>
);
})}
</tr>
{hasSecondDimension && (
<tr>
{injectSeparators(dimension2, styling.useSeparatorColumns).map(dimensionEntry => {
{injectSeparators(dimension2, styling.useSeparatorColumns).map((dimensionEntry, index) => {
if (dimensionEntry.isSeparator) {
const separatorStyle = {
color: 'white',
@@ -85,6 +88,7 @@ const HeadersTable = ({ data, general, styling }) => {
return (
<th
className="empty"
key={index}
style={separatorStyle}
>
*
@@ -120,6 +124,11 @@ HeadersTable.propTypes = {
})
}).isRequired,
general: PropTypes.shape({}).isRequired,
qlik: PropTypes.shape({
backendApi: PropTypes.shape({
selectValues: PropTypes.func.isRequired
}).isRequired
}).isRequired,
styling: PropTypes.shape({
headerOptions: PropTypes.shape({}),
options: PropTypes.shape({})

View File

@@ -25,6 +25,11 @@ export default {
qMeasures: []
}
},
support: {
export: true,
exportData: true,
snapshot: true
},
paint ($element, layout) {
try {
paint($element, layout, this);

View File

@@ -39,8 +39,8 @@ function getAlignment (option) {
function getFontSizeAdjustment (option) {
const fontSizeAdjustmentOptions = {
1: -2,
2: 0,
1: -1,
2: 1,
3: 2
};
@@ -91,24 +91,41 @@ function generateMeasurements (information) {
function generateDimensionEntry (information, data) {
return {
displayValue: data.qText,
elementNumber: data.qElemNumber,
name: information.qFallbackTitle,
value: data.qNum
};
}
function generateMatrixCell (information, data) {
return {
displayValue: data.qText,
elementNumber: data.qElemNumber,
format: information.format,
magnitude: information.magnitudeLabelSuffix.substring(
information.magnitudeLabelSuffix.length - 2,
information.magnitudeLabelSuffix.length - 1
function generateMatrixCell ({ cell, dimension1Information, dimension2Information, measurementInformation }) {
const matrixCell = {
displayValue: cell.qText,
format: measurementInformation.format,
magnitude: measurementInformation.magnitudeLabelSuffix.substring(
measurementInformation.magnitudeLabelSuffix.length - 2,
measurementInformation.magnitudeLabelSuffix.length - 1
),
magnitudeLabelSuffix: information.magnitudeLabelSuffix,
name: information.name,
value: data.qNum
magnitudeLabelSuffix: measurementInformation.magnitudeLabelSuffix,
name: measurementInformation.name,
parents: {
dimension1: {
elementNumber: dimension1Information.qElemNumber,
header: dimension1Information.qText
},
measurement: {
header: measurementInformation.name
}
},
value: cell.qNum
};
if (dimension2Information) {
matrixCell.parents.dimension2 = {
elementNumber: dimension2Information.qElemNumber
};
}
return matrixCell;
}
let lastRow = 0;
@@ -135,7 +152,14 @@ function generateDataSet (component, dimensionsInformation, measurementsInformat
.slice(firstDataCell, row.length)
.map((cell, cellIndex) => {
const measurementInformation = measurements[cellIndex];
const generatedCell = generateMatrixCell(measurementInformation, cell);
const dimension1Information = row[0]; // eslint-disable-line prefer-destructuring
const dimension2Information = hasSecondDimension ? row[1] : null;
const generatedCell = generateMatrixCell({
cell,
dimension1Information,
dimension2Information,
measurementInformation
});
return generatedCell;
});
@@ -228,9 +252,13 @@ async function initializeTransformed ({ $element, layout, component }) {
},
general: {
allowExcelExport: layout.allowexportxls,
allowFilteringByClick: layout.filteroncellclick,
cellSuffix: getCellSuffix(layout.columnwidthslider), // TOOD: move to matrix cells or is it headers.measurements?
errorMessage: layout.errormessage,
maxLoops
footnote: layout.footnote,
maxLoops,
subtitle: layout.subtitle,
title: layout.title
},
selection: {
dimensionSelectionCounts: dimensionsInformation.map(dimensionInfo => dimensionInfo.qStateCounts.qSelected)

View File

@@ -1,9 +1,7 @@
import $ from 'jquery';
import { enableExcelExport } from './excel-export';
import initializeStore from './store';
import React from 'react';
// import ReactDOM from 'react-dom';
import { renderToStaticMarkup } from 'react-dom/server';
import ReactDOM from 'react-dom';
import HeadersTable from './headers-table/index.jsx';
import DataTable from './data-table/index.jsx';
@@ -14,29 +12,20 @@ export default async function paint ($element, layout, component) {
layout
});
const {
ConceptMatrixColElem,
ConceptMatrixColElemTable,
ConceptMatrixRowElem
} = state.properties;
const {
data: { meta: { dimensionCount } },
selection: { dimensionSelectionCounts },
styling: { useSeparatorColumns }
} = state;
const jsx = (
<React.Fragment>
<div className="kpi-table">
<HeadersTable
data={state.data}
general={state.general}
qlik={component}
styling={state.styling}
/>
<DataTable
data={state.data}
general={state.general}
qlik={component}
renderData={false}
styling={state.styling}
/>
</div>
@@ -44,159 +33,52 @@ export default async function paint ($element, layout, component) {
<HeadersTable
data={state.data}
general={state.general}
qlik={component}
styling={state.styling}
/>
<DataTable
data={state.data}
general={state.general}
qlik={component}
styling={state.styling}
/>
</div>
</React.Fragment>
);
// TODO: switch to render when jquery interaction stuff in renderData is gone
const html = renderToStaticMarkup(jsx);
$element.html(html);
// ReactDOM.render(jsx, $element[0]);
ReactDOM.render(jsx, $element[0]);
RenderData();
// TODO: skipped the following as they weren't blockers for letting react handle rendering,
// they are however the only reason we still depend on jQuery and should be removed as part of unnecessary dependencies issue
$(`[tid="${layout.qInfo.qId}"] .data-table .row-wrapper`).on('scroll', function () {
$(`[tid="${layout.qInfo.qId}"] .kpi-table .row-wrapper`).scrollTop($(this).scrollTop());
});
// TODO: move jquery interactions into their respective components
// Hook up interactions and some html mangling
function RenderData () {
$('.data-table .row-wrapper').on('scroll', function () {
$(`[tid="${layout.qInfo.qId}"] .kpi-table .row-wrapper`).scrollTop($(this).scrollTop());
// freeze first column
$(`[tid="${layout.qInfo.qId}"] .qv-object-content-container`).on('scroll', (t) => {
$(`[tid="${layout.qInfo.qId}"] .kpi-table`).css('left', `${Math.round(t.target.scrollLeft)}px`);
});
// TODO: fixing tooltips has a seperate issue, make sure to remove this as part of that issue
$(`[tid="${layout.qInfo.qId}"] .header-wrapper th`).hover(function () {
$(`[tid="${layout.qInfo.qId}"] .tooltip`).delay(500)
.show(0);
$(`[tid="${layout.qInfo.qId}"] .header-wrapper th`).children(`[tid="${layout.qInfo.qId}"] .tooltip`)
.remove();
const element = $(this);
const offset = element.offset();
const toolTip = $('<div class="tooltip"></div>');
toolTip.css({
left: offset.left,
top: offset.top
});
// on hover popup with cell value, only in headers
$('.header-wrapper th').hover(function () {
$('.tooltip').delay(500)
.show(0);
$('.header-wrapper th').children('.tooltip')
.remove();
const element = $(this);
const offset = element.offset();
const toolTip = $("<div class='tooltip'></div>");
toolTip.css({
top: offset.top,
left: offset.left
});
toolTip.text(element.text());
$('.header-wrapper th').append(toolTip);
}, () => {
$('.tooltip').delay(0)
.hide(0);
});
// allow making selections inside the table
$('.data-table td').on('click', function () {
if (layout.filteroncellclick == false) {
return;
}
const indextr = $(this).parent()
.parent()
.children()
.index($(this).parent()); // identifica la row
const indextd = $(this).parent()
.children()
.index($(this)); // identifica la col
let SelectRow = 0;
let SelectCol = 0;
SelectRow = ConceptMatrixRowElem[(indextr)];
// este if verifica primero si hay selecciones hechas en la dimensión, si las hay
// las reselecciona para poder borrar antes de poder seleccionar lo que quiero
// no es viable pedirle que seleccione a la vez elementos de 2 selecciones, se queda
// colgado el menú de confirm, por eso uso este sistema, que sí funciona.
// it can cause issues like error messages and wrong selections if there are null values
// and the check allow null values is active
if (dimensionCount > 1 && indextd > 0) {
if (dimensionSelectionCounts[1] > 0) {
const SelectB = JSON.parse(JSON.stringify(ConceptMatrixColElemTable));
component.backendApi.selectValues(1, SelectB, true);
$(this).toggleClass('selected');
}
SelectCol = ConceptMatrixColElemTable[(indextd)];
component.backendApi.selectValues(1, [SelectCol], true);
$(this).toggleClass('selected');
}
if (indextd > 0 && dimensionSelectionCounts[0] > 0) {
const SelectA = JSON.parse(JSON.stringify(ConceptMatrixRowElem));
component.backendApi.selectValues(0, SelectA, true);
$(this).toggleClass('selected');
}
if (indextd > 0) {
component.backendApi.selectValues(0, [SelectRow], true);
$(this).toggleClass('selected');
}
});
// allow selections through the header of the second dimension
$('.header-wrapper th').on('click', function () {
const indextd = $(this).parent()
.children()
.index($(this)); // identifica la col
let SelectCol = 0;
if (dimensionCount > 1 && indextd > 0) {
if (dimensionSelectionCounts[1] > 0) {
const SelectB = JSON.parse(JSON.stringify(ConceptMatrixColElem));
component.backendApi.selectValues(1, SelectB, true);
$(this).toggleClass('selected');
}
if (useSeparatorColumns) {
SelectCol = ConceptMatrixColElem[(Math.round(indextd / 2) - 1)];
} else {
SelectCol = ConceptMatrixColElem[(Math.round(indextd) - 1)];
}
component.backendApi.selectValues(1, [SelectCol], true);
$(this).toggleClass('selected');
}
});
// allow selections in desc dimension cells
$('.kpi-table td').on('click', function () {
const indextr = $(this).parent()
.parent()
.children()
.index($(this).parent()); // identifica la row
let SelectRow = 0;
SelectRow = ConceptMatrixRowElem[(indextr)];
if (dimensionSelectionCounts[0] > 0) {
const SelectA = JSON.parse(JSON.stringify(ConceptMatrixRowElem));
component.backendApi.selectValues(0, SelectA, true);
$(this).toggleClass('selected');
}
component.backendApi.selectValues(0, [SelectRow], true);
$(this).toggleClass('selected');
});
enableExcelExport(layout, html);
// freeze first column
$('.qv-object-content-container').on('scroll', (t) => {
$('.kpi-table').css('left', `${Math.round(t.target.scrollLeft)}px`);
});
$('.kpi-table .row-wrapper tr').each(function () {
$(this).find('th:not(.fdim-cells)')
.remove();
$(this).find('td:not(.fdim-cells)')
.remove();
});
$('.kpi-table .header-wrapper tr').each(function () {
$(this).find('th:not(.fdim-cells)')
.remove();
});
}
toolTip.text(element.text());
$(`[tid="${layout.qInfo.qId}"] .header-wrapper th`).append(toolTip);
}, () => {
$(`[tid="${layout.qInfo.qId}"] .tooltip`).delay(0)
.hide(0);
});
}

View File

@@ -1,4 +1,3 @@
import { onlyUnique } from './utilities';
import initializeTransformed from './initialize-transformed';
async function initialize ({ $element, layout, component }) {
@@ -8,137 +7,7 @@ async function initialize ({ $element, layout, component }) {
layout
});
// TODO: remove everything from here to return statement once jquery parts in paint has been refactored
const vMaxLoops = layout.maxloops;
const vErrorMessage = layout.errormessage;
let vDimName = '';
const ConceptMatrixFirst = new Array();
const ConceptMatrixSecond = new Array();
const LabelsArray = new Array();
const ArrayGetSelectedCount = new Array();
let vNumDims = 0;
let vNumMeasures = 0;
const MeasuresFormat = new Array();
const dim_count = layout.qHyperCube.qDimensionInfo.length;
const measure_count = layout.qHyperCube.qMeasureInfo.length;
let vSeparatorCols = layout.separatorcols;
if (dim_count == 1) {
vSeparatorCols = false;
}
let lastrow = 0;
const ConceptMatrix = new Array();
let ConceptMatrixRowElem = new Array();
let ConceptMatrixColElem = new Array();
const ConceptMatrixColElemTable = new Array();
const ConceptMatrixPivot = new Array();
let ConceptMatrixFirstClean = new Array();
const nRows = component.backendApi.getRowCount();
const dimensionInfos = component.backendApi.getDimensionInfos();
LabelsArray.push(dimensionInfos[0].qFallbackTitle);
ArrayGetSelectedCount.concat(dimensionInfos.map(dimensionInfo => dimensionInfo.qStateCounts.qSelected));
vNumDims += dimensionInfos.length;
const measureInfos = component.backendApi.getMeasureInfos();
measureInfos.forEach(measureInfo => {
vDimName = measureInfo.qFallbackTitle;
LabelsArray.push(vDimName);
let mfor = '';
if (measureInfo.qNumFormat.qType == 'U' || measureInfo.qNumFormat.qFmt == '##############') {
mfor = '#.##0'; // in case of undefined
} else if (measureInfo.qNumFormat.qType == 'R') {
mfor = measureInfo.qNumFormat.qFmt;
mfor = mfor.replace(/(|)/gi, '');
} else {
mfor = measureInfo.qNumFormat.qFmt;
}
MeasuresFormat.push(mfor);
vNumMeasures++;
});
component.backendApi.eachDataRow((t, a) => {
lastrow = t;
const vNumMeasuresPlus = vNumMeasures + 1;
ConceptMatrix[t] = new Array();
ConceptMatrix[t][0] = a[0].qText;
ConceptMatrixFirst[t] = a[0].qText;
ConceptMatrixRowElem[t] = a[0].qElemNumber;
let nMeasures = 0;
if (vNumDims == 1) {
for (nMeasures = 1; nMeasures <= vNumMeasures; nMeasures++) {
ConceptMatrix[t][nMeasures] = a[nMeasures].qNum;
}
} else {
ConceptMatrix[t][1] = a[1].qText;
ConceptMatrixColElem[t] = a[1].qElemNumber;
ConceptMatrixSecond[t] = a[1].qText;
// set the hipercube in a plain array without pivoting
for (nMeasures = 2; nMeasures <= vNumMeasuresPlus; nMeasures++) {
ConceptMatrix[t][nMeasures] = a[nMeasures].qNum;
}
}
});
ConceptMatrixFirstClean = ConceptMatrixFirst.filter(onlyUnique);
if (nRows >= (vMaxLoops * 1000)) {
alert(vErrorMessage);
}
if (vNumDims == 2) {
// new array with unique values for 2nd dim
var SecondHeader = ConceptMatrixSecond.filter(onlyUnique);// second dimension concepts
ConceptMatrixRowElem = ConceptMatrixRowElem.filter(onlyUnique);// first dimension concepts
ConceptMatrixColElem = ConceptMatrixColElem.filter(onlyUnique);// dimension code for further selections
const eo = ConceptMatrixColElem.length;
let vLoopColsMeasures = 1;
ConceptMatrixColElemTable[0] = ConceptMatrixColElem[0];
for (let xx = 0; xx < eo; xx++) {
if (vSeparatorCols && xx > 0) {
ConceptMatrixColElemTable[vLoopColsMeasures] = ConceptMatrixColElem[xx];
vLoopColsMeasures++;
}
for (let xxx = 0; xxx < vNumMeasures; xxx++) {
ConceptMatrixColElemTable[vLoopColsMeasures] = ConceptMatrixColElem[xx];
vLoopColsMeasures++;
}
}
let ConceptPos = 0;
let nMeas3 = 0;
let vHeaderIndex = 0;
let MeasurePos = 0;
for (let nPivotElems = 0; nPivotElems <= lastrow; nPivotElems++) {
ConceptMatrixPivot[nPivotElems] = new Array();
ConceptPos = ConceptMatrixFirstClean.indexOf(ConceptMatrix[nPivotElems][0]);
ConceptMatrixPivot[ConceptPos][0] = ConceptMatrix[nPivotElems][0];
for (let nMeas2 = 1; nMeas2 <= measure_count; nMeas2++) {
nMeas3 = nMeas2 + 1;
vHeaderIndex = (SecondHeader.indexOf(ConceptMatrix[nPivotElems][1]) + 1);
MeasurePos = (vHeaderIndex * measure_count) + (nMeas2 - measure_count);
ConceptMatrixPivot[ConceptPos][MeasurePos] = ConceptMatrix[nPivotElems][nMeas3];
}
}
}
const properties = {
ConceptMatrixColElem,
ConceptMatrixColElemTable,
ConceptMatrixRowElem
};
return {
properties,
...transformedProperties
};
return transformedProperties;
}
export default initialize;