diff --git a/.circleci/config.yml b/.circleci/config.yml
index 040ea5c..c7735ea 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -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
diff --git a/assets/qlik-smart-pivot.qext b/assets/qlik-smart-pivot.qext
index c980707..4c1f96f 100644
--- a/assets/qlik-smart-pivot.qext
+++ b/assets/qlik-smart-pivot.qext
@@ -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",
diff --git a/settings.js b/settings.js
index 842ebc1..0b1583d 100644
--- a/settings.js
+++ b/settings.js
@@ -4,10 +4,10 @@ const packageJSON = require('./package.json');
const defaultBuildDestination = path.resolve("./build");
module.exports = {
- buildDestination: process.env.BUILD_PATH || defaultBuildDestination,
+ buildDestination: "C:\\Users\\Ahmed\\Documents\\Qlik\\Sense\\Extensions\\qlik-smart-pivot",
mode: process.env.NODE_ENV || 'development',
name: packageJSON.name,
version: process.env.VERSION || 'local-dev',
url: process.env.BUILD_URL || defaultBuildDestination,
- port: process.env.PORT || 8085
+ port: process.env.PORT || 8090
};
diff --git a/src/data-table/data-cell.jsx b/src/data-table/data-cell.jsx
index babee8b..cae3f2f 100644
--- a/src/data-table/data-cell.jsx
+++ b/src/data-table/data-cell.jsx
@@ -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,52 +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, semaphoreColors: { fieldsToApplyTo } } = styling;
- const isValidSemaphoreValue = !styleBuilder.hasComments() && !isNaN(measurement.value);
- const shouldHaveSemaphoreColors = fieldsToApplyTo.applyToMetric && (fieldsToApplyTo.applyToAll || fieldsToApplyTo.specificFields.indexOf(measurement.name) !== -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'
- };
+ qlik.backendApi.selectValues(0, [measurement.parents.dimension1.elementNumber], true);
+
+ if (hasSecondDimension) {
+ qlik.backendApi.selectValues(1, [measurement.parents.dimension2.elementNumber], true);
+ }
}
- 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';
+ render () {
+ const { data, general, measurement, styleBuilder, styling } = this.props;
+ const isColumnPercentageBased = measurement.name.substring(0, 1) === '%';
+ let formattedMeasurementValue = formatMeasurementValue(measurement, styling);
+ if (styleBuilder.hasComments()) {
+ formattedMeasurementValue = '.';
+ }
+ let textAlignment = 'Right';
+ const textAlignmentProp = styling.options.textAlignment;
+ if (textAlignmentProp) {
+ textAlignment = textAlignmentProp;
+ }
+
+ const { semaphoreColors, semaphoreColors: { fieldsToApplyTo } } = styling;
+ const isValidSemaphoreValue = !styleBuilder.hasComments() && !isNaN(measurement.value);
+ const shouldHaveSemaphoreColors = fieldsToApplyTo.applyToMetric && (fieldsToApplyTo.applyToAll || fieldsToApplyTo.specificFields.indexOf(measurement.name) !== -1);
+ let cellStyle;
+ if (isValidSemaphoreValue && shouldHaveSemaphoreColors) {
+ const { backgroundColor, color } = getSemaphoreColors(measurement, semaphoreColors);
+ cellStyle = {
+ backgroundColor,
+ color,
+ fontFamily: styling.options.fontFamily,
+ ...styleBuilder.getStyle(),
+ paddingLeft: '4px',
+ textAlign: textAlignment
+ };
+ }
+
+ 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 (
+
+ {formattedMeasurementValue}
+ |
+ );
}
-
- return (
-
- {formattedMeasurementValue}
- |
- );
-};
-
+}
DataCell.propTypes = {
data: PropTypes.shape({
headers: PropTypes.shape({
@@ -122,6 +144,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,
diff --git a/src/data-table/index.jsx b/src/data-table/index.jsx
index afe4aa5..5da451c 100644
--- a/src/data-table/index.jsx
+++ b/src/data-table/index.jsx
@@ -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 (
- {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()
- };
+
+ {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 (
-
- |
-
+
- {dimensionEntry.displayValue}
- |
- {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 (
+
+ *
+ |
+ );
+ }
+ const { dimension1: dimension1Info, dimension2, measurement } = measurementData.parents;
+ const id = `${dimension1Info.elementNumber}-${dimension2 && dimension2.elementNumber}-${measurement.header}`;
return (
-
- *
- |
+
);
- }
- return (
-
- );
- })}
-
- );
- })}
+ })}
+
+ );
+ })}
+
);
};
+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
diff --git a/src/data-table/row-header.jsx b/src/data-table/row-header.jsx
new file mode 100644
index 0000000..06da248
--- /dev/null
+++ b/src/data-table/row-header.jsx
@@ -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 (
+
+
+ {entry.displayValue}
+ |
+ );
+ }
+}
+
+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;
diff --git a/src/definition/formatted.js b/src/definition/formatted.js
index 3e1cce6..5997e36 100644
--- a/src/definition/formatted.js
+++ b/src/definition/formatted.js
@@ -139,6 +139,10 @@ const formatted = {
component: 'dropdown',
label: 'FontFamily',
options: [
+ {
+ value: 'QlikView Sans',
+ label: 'QlikView Sans'
+ },
{
value: 'Arial',
label: 'Arial'
@@ -164,7 +168,7 @@ const formatted = {
label: 'Verdana'
}
],
- defaultValue: 'Calibri'
+ defaultValue: 'QlikView Sans'
},
DataFontSize: {
ref: 'lettersize',
@@ -181,7 +185,27 @@ const formatted = {
label: 'Medium'
}
],
- defaultValue: 2
+ defaultValue: 1
+ },
+ textAlignment: {
+ ref: 'cellTextAlignment',
+ label: 'Cell Text alignment',
+ component: 'buttongroup',
+ options: [
+ {
+ value: 'left',
+ label: 'Left'
+ },
+ {
+ value: 'center',
+ label: 'Center'
+ },
+ {
+ value: 'right',
+ label: 'Right'
+ }
+ ],
+ defaultValue: 'right'
},
ColumnWidthSlider: {
type: 'number',
diff --git a/src/definition/header.js b/src/definition/header.js
index ebf830a..fd61a63 100644
--- a/src/definition/header.js
+++ b/src/definition/header.js
@@ -27,7 +27,7 @@ const header = {
ref: 'HeaderColorSchema',
type: 'string',
component: 'dropdown',
- label: 'BackGround Header Color',
+ label: 'Background Header Color',
options: [
{
value: 'Clean',
diff --git a/src/definition/index.js b/src/definition/index.js
index fd6a2a9..9352f61 100644
--- a/src/definition/index.js
+++ b/src/definition/index.js
@@ -9,15 +9,16 @@ import pijamaColorLibrary from './pijama-color-library';
const definition = {
component: 'accordion',
items: {
- dimensions: {
- max: 2,
- min: 1,
- uses: 'dimensions'
- },
- measures: {
- max: 9,
- min: 1,
- uses: 'measures'
+ data: {
+ items: {
+ dimensions: {
+ disabledRef: ''
+ },
+ measures: {
+ disabledRef: ''
+ }
+ },
+ uses: 'data'
},
settings: {
items: {
diff --git a/src/excel-export.js b/src/excel-export.js
index 9734e8c..413f391 100644
--- a/src/excel-export.js
+++ b/src/excel-export.js
@@ -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 += '';
- myTitle += layout.title;
- myTitle += '
';
- }
- if (layout.subtitle.length > 0) {
- mySubTitle += '';
- mySubTitle += layout.subtitle;
- mySubTitle += '
';
- }
- if (layout.footnote.length > 0) {
- myFootNote += 'Note:';
- myFootNote += layout.footnote;
- myFootNote += '
';
- }
-
- $('.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 = '';
- vEncodeHead += myTitle + mySubTitle + myFootNote;
- const vEncode = encodeURIComponent($clonedDiv.html());
- let vDecode = `${vEncodeHead + vEncode}`;
-
- $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 = '';
- a += myTitle + mySubTitle + myFootNote;
- a += f;
- a = a.split('>.<').join('><');
- a += '';
-
- 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 = '';
- vEncodeHead += myTitle + mySubTitle + myFootNote;
- const vEncode = encodeURIComponent($clonedDiv.html());
- let vDecode = `${vEncodeHead + vEncode}`;
-
- $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 = `${title}
`;
+ const subtitleHTML = `${subtitle}
`;
+ const footnoteHTML = `Note:${footnote}
`;
+ const dataTableClone = document.querySelector('.data-table').cloneNode(true);
+
+ removeAllTooltips(dataTableClone);
+
+ const tableHTML = `
+
+
+
+
+
+
+ ${titleHTML.length > 0 ? titleHTML : ''}
+ ${subtitleHTML.length > 0 ? subtitleHTML : ''}
+ ${footnoteHTML.length > 0 ? footnoteHTML : ''}
+ ${dataTableClone.outerHTML}
+
+
+ `.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);
+}
diff --git a/src/export-button.jsx b/src/export-button.jsx
index 60ea0d2..76436ed 100644
--- a/src/export-button.jsx
+++ b/src/export-button.jsx
@@ -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 && (
@@ -20,7 +34,8 @@ ExportButton.defaultProps = {
};
ExportButton.propTypes = {
- excelExport: PropTypes.bool
+ excelExport: PropTypes.bool,
+ general: PropTypes.shape({}).isRequired
};
export default ExportButton;
diff --git a/src/headers-table/column-header.jsx b/src/headers-table/column-header.jsx
index 3c9f251..d50d666 100644
--- a/src/headers-table/column-header.jsx
+++ b/src/headers-table/column-header.jsx
@@ -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 (
-
- {title}
- |
- );
-};
+ 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 (
+
+ {entry.displayValue}
+ |
+ );
+ }
+}
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;
diff --git a/src/headers-table/export-column-header.jsx b/src/headers-table/export-column-header.jsx
index 640e89e..96fe045 100644
--- a/src/headers-table/export-column-header.jsx
+++ b/src/headers-table/export-column-header.jsx
@@ -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}
>
-
+
{title}
);
@@ -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({
diff --git a/src/headers-table/index.jsx b/src/headers-table/index.jsx
index c45fa48..8ca4bb4 100644
--- a/src/headers-table/index.jsx
+++ b/src/headers-table/index.jsx
@@ -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 }) => {
{
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 (
*
@@ -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}
/>
);
})}
{hasSecondDimension && (
|
- {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 (
*
@@ -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({})
diff --git a/src/index.js b/src/index.js
index 0ead03a..ac5fb0a 100644
--- a/src/index.js
+++ b/src/index.js
@@ -12,6 +12,18 @@ export default {
'$timeout',
function () { }
],
+ data: {
+ dimensions: {
+ max: 2,
+ min: 1,
+ uses: 'dimensions'
+ },
+ measures: {
+ max: 9,
+ min: 1,
+ uses: 'measures'
+ }
+ },
definition,
initialProperties: {
qHyperCubeDef: {
@@ -25,6 +37,11 @@ export default {
qMeasures: []
}
},
+ support: {
+ export: true,
+ exportData: true,
+ snapshot: true
+ },
paint ($element, layout) {
try {
paint($element, layout, this);
diff --git a/src/initialize-transformed.js b/src/initialize-transformed.js
index b8aaff4..ec6f244 100644
--- a/src/initialize-transformed.js
+++ b/src/initialize-transformed.js
@@ -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)
@@ -254,7 +282,8 @@ async function initializeTransformed ({ $element, layout, component }) {
backgroundColorOdd: colors[`vColLib${layout.ColorSchema}P`],
color: layout.BodyTextColorSchema,
fontFamily: layout.FontFamily,
- fontSizeAdjustment: getFontSizeAdjustment(layout.lettersize)
+ fontSizeAdjustment: getFontSizeAdjustment(layout.lettersize),
+ textAlignment: layout.cellTextAlignment
},
semaphoreColors: {
fieldsToApplyTo: {
diff --git a/src/main.less b/src/main.less
index b80d001..e07f9bf 100644
--- a/src/main.less
+++ b/src/main.less
@@ -8,6 +8,9 @@
box-sizing: border-box;
}
+ .edit-mode{
+ pointer-events: none;
+ }
._cell(@Width: 50px) {
min-width: @Width!important;
max-width: @Width!important;
diff --git a/src/paint.jsx b/src/paint.jsx
index 2ed949d..2255a89 100644
--- a/src/paint.jsx
+++ b/src/paint.jsx
@@ -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';
@@ -13,190 +11,74 @@ export default async function paint ($element, layout, component) {
component,
layout
});
-
- const {
- ConceptMatrixColElem,
- ConceptMatrixColElemTable,
- ConceptMatrixRowElem
- } = state.properties;
-
- const {
- data: { meta: { dimensionCount } },
- selection: { dimensionSelectionCounts },
- styling: { useSeparatorColumns }
- } = state;
-
+ const editmodeClass = component.inAnalysisState() ? '' : 'edit-mode';
const jsx = (
-
+
-
+
);
- // 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 = $(' ');
+
+ 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 = $(" ");
-
- 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);
+ });
}
diff --git a/src/store.js b/src/store.js
index 7f4b3f3..6987015 100644
--- a/src/store.js
+++ b/src/store.js
@@ -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;
|