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 ( - - - {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 ( - - ); - })} - - ); - })} + })} + + ); + })} +
- + - {dimensionEntry.displayValue} - + * + - * -
); }; +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;