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/src/data-table/data-cell.jsx b/src/data-table/data-cell.jsx index 272f813..3a50bc0 100644 --- a/src/data-table/data-cell.jsx +++ b/src/data-table/data-cell.jsx @@ -95,12 +95,17 @@ class DataCell extends React.PureComponent { if (styleBuilder.hasComments()) { formattedMeasurementValue = '.'; } + let textAlignment = 'Right'; + const textAlignmentProp = styling.options.textAlignment; + if (textAlignmentProp) { + textAlignment = textAlignmentProp; + } let cellStyle = { fontFamily: styling.options.fontFamily, ...styleBuilder.getStyle(), paddingLeft: '4px', - textAlign: 'right' + textAlign: textAlignment }; const { semaphoreColors } = styling; @@ -114,7 +119,7 @@ class DataCell extends React.PureComponent { fontFamily: styling.options.fontFamily, fontSize: styleBuilder.getStyle().fontSize, paddingLeft: '4px', - textAlign: 'right' + textAlign: textAlignment }; } 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/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/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 019a0f1..8ca4bb4 100644 --- a/src/headers-table/index.jsx +++ b/src/headers-table/index.jsx @@ -29,6 +29,7 @@ const HeadersTable = ({ data, general, qlik, styling }) => { dimensionInfo.qStateCounts.qSelected) @@ -279,7 +282,8 @@ async function initializeTransformed ({ $element, layout, component }) { backgroundColorOdd: colors[`vColLib${layout.ColorSchemaP}`], color: layout.BodyTextColorSchema, fontFamily: layout.FontFamily, - fontSizeAdjustment: getFontSizeAdjustment(layout.lettersize) + fontSizeAdjustment: getFontSizeAdjustment(layout.lettersize), + textAlignment: layout.cellTextAlignment }, semaphoreColors: { fieldsToApplyTo: { diff --git a/src/paint.jsx b/src/paint.jsx index c58a12b..5428149 100644 --- a/src/paint.jsx +++ b/src/paint.jsx @@ -48,7 +48,6 @@ export default async function paint ($element, layout, component) { ReactDOM.render(jsx, $element[0]); - // 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 () {