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