Compare commits

..

1 Commits

Author SHA1 Message Date
Albert Backenhof
f8857b1594 Adjusted header height for medium font size
-The size pushed the lower end of the row-wrapper
 outside of the parent container, which was caused
 by the heights not adding up.

Issue: DEB-163
2019-04-10 11:23:11 +02:00
27 changed files with 625 additions and 680 deletions

View File

@@ -1,22 +1,52 @@
# P&L Smart Pivot, a Qlik Sense Extension for Financial reporting
This extension is part of the extension bundles for Qlik Sense. The repository is maintained and moderated by Qlik RD.
[![CircleCI](https://circleci.com/gh/qlik-oss/PLSmartPivot.svg?style=svg)](https://circleci.com/gh/qlik-oss/PLSmartPivot)
Feel free to fork and suggest pull requests for improvements and bug fixes. Changes will be moderated and reviewed before inclusion in future bundle versions. Please note that emphasis is on backward compatibility, i.e. breaking changes will most likely not be approved.
This extension is useful to create reports where the look&feel is rellevantand and pivot a second dimension is needed. Based on P&L Smart.
It's specifically focused on financial reports, trying to solve some common needs of this area:
- smart export to excel
- easy creation of reports
- custom corporate reporting (bold, italic, background color, letter size, headers,...)
- selections inside the reports
- custom external templates
- analytical reports
# Manual
You'll find a manual [Qlik Sense P&LSmart Pivot Extension Manual.pdf](resources/Qlik Sense P&LSmart Pivot Extension Manual.pdf) and one app example [P&LSmartPivot_demo.qvf](resources/P&LSmartPivot_demo.qvf).
# Installation
1. Download the extension zip, `qlik-smart-pivot_<version>.zip`, from the latest release(https://github.com/qlik-oss/PLSmartPivot/releases/latest)
2. Install the extension:
a. **Qlik Sense Desktop**: unzip to a directory under [My Documents]/Qlik/Sense/Extensions.
b. **Qlik Sense Server**: import the zip file in the QMC.
Usage documentation for the extension is available at https://help.qlik.com.
# Developing the extension
If you want to do code changes to the extension follow these simple steps to get going.
1. Get Qlik Sense Desktop
1. Create a new app and add P&L pivot to a sheet.
1. Create a new app and add the extension to a sheet.
2. Clone the repository
3. Run `npm install`
4. Run `npm run build` - to build a dev-version to the /dist folder.
5. Move the content of the /dist folder to the extension directory. Usually in `C:/Users/<user>/Documents/Qlik/Sense/Extensions/qlik-smart-pivot`.
4. Set the environment variable `BUILD_PATH` to your extensions directory. It will be something like `C:/Users/<user>/Documents/Qlik/Sense/Extensions/<extension_name>`.
5. You now have two options. Either run the watch task or the build task. They are explained below. Both of them default to development mode but can be run in production by setting `NODE_ENV=production` before running the npm task.
a. **Watch**: `npm run watch`. This will start a watcher which will rebuild the extension and output all needed files to the `buildFolder` for each code change you make. See your changes directly in your Qlik Sense app.
b. **Build**: `npm run build`. If you want to build the extension package. The output zip-file can be found in the `buildFolder`.
# Original authors
[github.com/iviasensio](https://github.com/iviasensio)
# License
Released under the [MIT License](LICENSE).
Released under the [MIT License](LICENSE).

View File

@@ -15,7 +15,7 @@ gulp.task('qext', function () {
type: 'visualization',
description: pkg.description + '\nVersion: ' + VERSION,
version: VERSION,
icon: 'pivot-table',
icon: 'table',
preview: 'qlik-smart-pivot.png',
keywords: 'qlik-sense, visualization',
author: pkg.author,

View File

@@ -54,20 +54,21 @@ class DataCell extends React.PureComponent {
formattedMeasurementValue = formatMeasurementValue(measurement, styling);
}
const { conditionalColoring } = styling;
if (conditionalColoring.enabled) {
const isValidConditionalColoringValue = !styleBuilder.hasComments() && !isNaN(measurement.value);
const isSpecifiedRow =
conditionalColoring.rows.indexOf(measurement.parents.dimension1.header) !== -1;
const isSpecifiedMeasure =
conditionalColoring.measures.indexOf(measurement.parents.measurement.index) !== -1;
const shouldHaveConditionalColoring = (conditionalColoring.colorAllRows || isSpecifiedRow)
&& (conditionalColoring.colorAllMeasures || isSpecifiedMeasure);
if (isValidConditionalColoringValue && shouldHaveConditionalColoring) {
const { color, textColor } = getConditionalColor(measurement, conditionalColoring);
cellStyle.backgroundColor = color.color;
cellStyle.color = textColor.color;
}
const { semaphoreColors, semaphoreColors: { fieldsToApplyTo } } = styling;
const isValidSemaphoreValue = !styleBuilder.hasComments() && !isNaN(measurement.value);
const dimension1Row = measurement.parents.dimension1.elementNumber;
const isSpecifiedMetricField = fieldsToApplyTo.metricsSpecificFields.indexOf(dimension1Row) !== -1;
const shouldHaveSemaphoreColors = (fieldsToApplyTo.applyToMetric || isSpecifiedMetricField);
if (isValidSemaphoreValue && shouldHaveSemaphoreColors) {
const { backgroundColor, color } = getSemaphoreColors(measurement, semaphoreColors);
cellStyle = {
...styleBuilder.getStyle(),
backgroundColor,
color,
fontFamily: styling.options.fontFamily,
paddingLeft: '5px',
textAlign: textAlignment
};
}
let cellClass = 'grid-cells';
@@ -84,7 +85,6 @@ class DataCell extends React.PureComponent {
style={cellStyle}
>
<Tooltip
styling={styling}
tooltipText={formattedMeasurementValue}
>
{formattedMeasurementValue}
@@ -176,12 +176,12 @@ function formatMeasurementValue (measurement, styling) {
return formattedMeasurementValue;
}
function getConditionalColor (measurement, conditionalColoring) {
if (measurement.value < conditionalColoring.threshold.poor) {
return conditionalColoring.colors.poor;
function getSemaphoreColors (measurement, semaphoreColors) {
if (measurement.value < semaphoreColors.status.critical) {
return semaphoreColors.statusColors.critical;
}
if (measurement.value < conditionalColoring.threshold.fair) {
return conditionalColoring.colors.fair;
if (measurement.value < semaphoreColors.status.medium) {
return semaphoreColors.statusColors.medium;
}
return conditionalColoring.colors.good;
return semaphoreColors.statusColors.normal;
}

View File

@@ -40,15 +40,13 @@ const DataTable = ({ data, general, qlik, renderData, styling }) => {
return (
<tr key={dimensionEntry.displayValue}>
{!renderData ?
<RowHeader
entry={dimensionEntry}
qlik={qlik}
rowStyle={rowStyle}
styleBuilder={styleBuilder}
styling={styling}
/> : null
}
<RowHeader
entry={dimensionEntry}
qlik={qlik}
rowStyle={rowStyle}
styleBuilder={styleBuilder}
styling={styling}
/>
{renderData && injectSeparators(
matrix[dimensionIndex],
styling.useSeparatorColumns,

View File

@@ -27,7 +27,6 @@ class RowHeader extends React.PureComponent {
>
<Tooltip
isTooltipActive={!inEditState}
styling={styling}
tooltipText={entry.displayValue}
>
<HeaderPadding

View File

@@ -6,7 +6,7 @@ function createCube (definition, app) {
});
}
async function buildDataCube (originCubeDefinition, hasTwoDimensions, app) {
async function buildDataCube (originCubeDefinition, dimensionIndexes, app) {
const cubeDefinition = {
...originCubeDefinition,
qInitialDataFetch: [
@@ -15,50 +15,77 @@ async function buildDataCube (originCubeDefinition, hasTwoDimensions, app) {
qWidth: 10
}
],
qDimensions: [originCubeDefinition.qDimensions[0]],
qDimensions: [originCubeDefinition.qDimensions[dimensionIndexes.dimension1]],
qMeasures: originCubeDefinition.qMeasures
};
if (hasTwoDimensions) {
cubeDefinition.qDimensions.push(originCubeDefinition.qDimensions[1]);
if (dimensionIndexes.dimension2) {
cubeDefinition.qDimensions.push(originCubeDefinition.qDimensions[dimensionIndexes.dimension2]);
}
const cube = await createCube(cubeDefinition, app);
return cube.qHyperCube.qDataPages[0].qMatrix;
}
export async function initializeDataCube (component, layout) {
if (component.backendApi.isSnapshot) {
return layout.snapshotData.dataCube;
}
const app = qlik.currApp(component);
const properties = (await component.backendApi.getProperties());
// If this is a master object, fetch the hyperCubeDef of the original object
const hyperCubeDef = properties.qExtendsId
? (await app.getObjectProperties(properties.qExtendsId)).properties.qHyperCubeDef
: properties.qHyperCubeDef;
return buildDataCube(hyperCubeDef, layout.qHyperCube.qDimensionInfo.length === 2, app);
}
export function initializeDesignList (component, layout) {
if (component.backendApi.isSnapshot) {
return layout.snapshotData.designList;
}
if (!layout.stylingfield) {
async function buildDesignCube (originCubeDefinition, dimensionIndexes, app) {
if (!dimensionIndexes.design) {
return null;
}
const cube = await createCube({
qInitialDataFetch: [
{
qHeight: 1000,
qWidth: 1
}
],
qDimensions: [originCubeDefinition.qDimensions[dimensionIndexes.design]]
}, app);
return new Promise(resolve => {
const app = qlik.currApp(component);
const stylingField = app.field(layout.stylingfield);
const listener = function () {
const data = stylingField.rows.map(row => row.qText);
stylingField.OnData.unbind(listener);
resolve(data);
};
stylingField.OnData.bind(listener);
stylingField.getData();
});
return cube.qHyperCube.qDataPages[0].qMatrix;
}
const STYLE_SEPARATOR_COUNT = 7;
function findDesignDimension (qMatrix) {
return qMatrix[0].map(entry => (entry.qText.match(/;/g) || []).length).indexOf(STYLE_SEPARATOR_COUNT);
}
function getDimensionIndexes (dimensionsInformation, designDimensionIndex) {
const hasDesign = designDimensionIndex !== -1;
const nonDesignDimensionCount = hasDesign ? dimensionsInformation.length - 1 : dimensionsInformation.length;
const dimension1 = designDimensionIndex === 0 ? 1 : 0;
let dimension2 = false;
if (nonDesignDimensionCount === 2) {
dimension2 = hasDesign && designDimensionIndex < 2 ? 2 : 1;
}
const design = hasDesign && designDimensionIndex;
const firstMeasurementIndex = dimensionsInformation.length;
return {
design,
dimension1,
dimension2,
firstMeasurementIndex
};
}
export async function initializeCubes ({ component, layout }) {
const app = qlik.currApp(component);
const designDimensionIndex = findDesignDimension(layout.qHyperCube.qDataPages[0].qMatrix);
const dimensionsInformation = layout.qHyperCube.qDimensionInfo;
const dimensionIndexes = getDimensionIndexes(dimensionsInformation, designDimensionIndex);
let properties;
if (component.backendApi.isSnapshot) {
// Fetch properties of source
properties = (await app.getObjectProperties(layout.sourceObjectId)).properties;
} else {
properties = await component.backendApi.getProperties();
}
const originCubeDefinition = properties.qHyperCubeDef;
const designCube = await buildDesignCube(originCubeDefinition, dimensionIndexes, app);
const dataCube = await buildDataCube(originCubeDefinition, dimensionIndexes, app);
return {
design: designCube,
data: dataCube
};
}

View File

@@ -0,0 +1,96 @@
const conceptSemaphores = {
items: {
AllConcepts: {
component: 'switch',
defaultValue: true,
label: 'All concepts affected',
options: [
{
label: 'On',
value: true
},
{
label: 'Off',
value: false
}
],
ref: 'allsemaphores',
type: 'boolean'
},
ConceptsAffected1: {
defaultValue: '',
ref: 'conceptsemaphore1',
show: data => !data.allsemaphores,
translation: 'Concept 1',
type: 'string'
},
ConceptsAffected2: {
defaultValue: '',
ref: 'conceptsemaphore2',
show: data => !data.allsemaphores,
translation: 'Concept 2',
type: 'string'
},
ConceptsAffected3: {
defaultValue: '',
ref: 'conceptsemaphore3',
show: data => !data.allsemaphores,
translation: 'Concept 3',
type: 'string'
},
ConceptsAffected4: {
defaultValue: '',
ref: 'conceptsemaphore4',
show: data => !data.allsemaphores,
translation: 'Concept 4',
type: 'string'
},
ConceptsAffected5: {
defaultValue: '',
ref: 'conceptsemaphore5',
show: data => !data.allsemaphores,
translation: 'Concept 5',
type: 'string'
},
ConceptsAffected6: {
defaultValue: '',
ref: 'conceptsemaphore6',
show: data => !data.allsemaphores,
translation: 'Concept 6',
type: 'string'
},
ConceptsAffected7: {
defaultValue: '',
ref: 'conceptsemaphore7',
show: data => !data.allsemaphores,
translation: 'Concept 7',
type: 'string'
},
ConceptsAffected8: {
defaultValue: '',
ref: 'conceptsemaphore8',
show: data => !data.allsemaphores,
translation: 'Concept 8',
type: 'string'
},
ConceptsAffected9: {
defaultValue: '',
ref: 'conceptsemaphore9',
show: data => !data.allsemaphores,
translation: 'Concept 9',
type: 'string'
},
// eslint-disable-next-line sort-keys
ConceptsAffected10: {
defaultValue: '',
ref: 'conceptsemaphore10',
show: data => !data.allsemaphores,
translation: 'Concept 10',
type: 'string'
}
},
label: 'Concept Semaphores',
type: 'items'
};
export default conceptSemaphores;

View File

@@ -1,199 +0,0 @@
const conditionalColoring = {
type: 'items',
label: 'Color by performance',
items: {
Enabled: {
ref: 'conditionalcoloring.enabled',
type: 'boolean',
label: 'Enabled',
component: 'switch',
defaultValue: false,
options: [
{
value: true,
label: 'On'
},
{
value: false,
label: 'Off'
}
]
},
ColorAllRows: {
ref: 'conditionalcoloring.colorall',
type: 'boolean',
label: 'Color all rows',
component: 'switch',
defaultValue: true,
options: [
{
value: true,
label: 'All rows'
},
{
value: false,
label: 'Specified rows'
}
],
show (data) {
return data.conditionalcoloring.enabled;
}
},
Rows: {
type: 'array',
ref: 'conditionalcoloring.rows',
label: 'Rows to color',
itemTitleRef: function (data) {
return data.rowname;
},
allowAdd: true,
allowRemove: true,
addTranslation: 'Add row to color',
items: {
Row: {
ref: 'rowname',
label: 'Name of row',
type: 'string',
defaultValue: ''
}
},
show (data) {
return data.conditionalcoloring.enabled && !data.conditionalcoloring.colorall;
}
},
ColorAllMeasures: {
ref: 'conditionalcoloring.colorallmeasures',
type: 'boolean',
label: 'Color all measures',
component: 'switch',
defaultValue: true,
options: [
{
value: true,
label: 'All measures'
},
{
value: false,
label: 'Specified measures'
}
],
show (data) {
return data.conditionalcoloring.enabled;
}
},
Measures: {
ref: 'conditionalcoloring.measures',
translation: 'Measures by index (ex: 0,3)',
type: 'string',
defaultValue: '',
show (data) {
return data.conditionalcoloring.enabled
&& data.conditionalcoloring.colorallmeasures === false;
}
},
ThresholdPoor: {
ref: 'conditionalcoloring.threshold_poor',
translation: 'Poor range limit',
type: 'number',
defaultValue: -0.1,
show (data) {
return data.conditionalcoloring.enabled;
}
},
ColorPoor: {
ref: 'conditionalcoloring.color_poor',
label: 'Poor background color',
type: 'object',
component: 'color-picker',
dualOutput: true,
defaultValue: {
index: 10,
color: '#f93f17'
},
show (data) {
return data.conditionalcoloring.enabled;
}
},
TextColorPoor: {
ref: 'conditionalcoloring.textcolor_poor',
label: 'Poor text color',
type: 'object',
component: 'color-picker',
dualOutput: true,
defaultValue: {
index: 1,
color: '#ffffff'
},
show (data) {
return data.conditionalcoloring.enabled;
}
},
ThresholdFair: {
ref: 'conditionalcoloring.threshold_fair',
translation: 'Fair range limit',
type: 'number',
defaultValue: 0,
show (data) {
return data.conditionalcoloring.enabled;
}
},
ColorFair: {
ref: 'conditionalcoloring.color_fair',
label: 'Fair background color',
type: 'object',
component: 'color-picker',
dualOutput: true,
defaultValue: {
index: 8,
color: '#ffcf02'
},
show (data) {
return data.conditionalcoloring.enabled;
}
},
TextColorFair: {
ref: 'conditionalcoloring.textcolor_fair',
label: 'Fair text color',
type: 'object',
component: 'color-picker',
dualOutput: true,
defaultValue: {
index: 15,
color: '#000000'
},
show (data) {
return data.conditionalcoloring.enabled;
}
},
ColorGood: {
ref: 'conditionalcoloring.color_good',
label: 'Good background color',
type: 'object',
component: 'color-picker',
dualOutput: true,
defaultValue: {
index: 3,
color: '#276e27'
},
show (data) {
return data.conditionalcoloring.enabled;
}
},
TextColorGood: {
ref: 'conditionalcoloring.textcolor_good',
label: 'Good text color',
type: 'object',
component: 'color-picker',
dualOutput: true,
defaultValue: {
index: 1,
color: '#ffffff'
},
show (data) {
return data.conditionalcoloring.enabled;
}
}
}
};
export default conditionalColoring;

View File

@@ -1,10 +1,10 @@
const header = {
type: 'items',
label: 'Header format',
label: 'Header Format',
items: {
Align: {
ref: 'HeaderAlign',
translation: 'Header alignment',
translation: 'Header Alignment',
type: 'number',
component: 'buttongroup',
options: [
@@ -29,14 +29,14 @@ const header = {
index: 6,
color: '#4477aa'
},
label: 'Background color',
label: 'Background Header Color',
ref: 'HeaderColorSchema',
type: 'object',
dualOutput: true
},
HeaderTextColor: {
ref: 'HeaderTextColorSchema',
label: 'Text color',
label: 'Text Header Color',
component: 'color-picker',
defaultValue: {
index: 1,
@@ -47,7 +47,7 @@ const header = {
},
HeaderFontSize: {
ref: 'lettersizeheader',
translation: 'Font size',
translation: 'Font Size',
type: 'number',
component: 'buttongroup',
options: [

View File

@@ -1,7 +1,8 @@
import pagination from './pagination';
import header from './header';
import tableFormat from './table-format';
import conditionalColoring from './conditional-coloring';
import conceptSemaphores from './concept-semaphores';
import metricSemaphores from './metric-semaphores';
const definition = {
component: 'accordion',
@@ -17,37 +18,18 @@ const definition = {
},
uses: 'data'
},
sorting: {
uses: 'sorting'
},
settings: {
items: {
ConceptSemaphores: conceptSemaphores,
Formatted: tableFormat,
Header: header,
ConditionalColoring: conditionalColoring,
MetricSemaphores: metricSemaphores,
Pagination: pagination
},
uses: 'settings'
},
about: {
component: 'items',
label: 'About',
items: {
header: {
label: 'P&L pivot',
style: 'header',
component: 'text'
},
paragraph1: {
label: `P&L pivot is a Qlik Sense extension which allows you to display Profit & Loss
reporting with color and font customizations.`,
component: 'text'
},
paragraph2: {
label: 'P&L pivot is based upon an extension created by Ivan Felipe Asensio.',
component: 'text'
}
}
sorting: {
uses: 'sorting'
}
},
type: 'items'

View File

@@ -0,0 +1,112 @@
const metricSemaphores = {
type: 'items',
label: 'Metric Semaphores',
items: {
AllMetrics: {
ref: 'allmetrics',
type: 'boolean',
component: 'switch',
label: 'All metrics affected',
options: [
{
value: true,
label: 'On'
},
{
value: false,
label: 'Off'
}
],
defaultValue: false
},
MetricsAffected: {
ref: 'metricssemaphore',
translation: 'Metrics affected (1,2,4,...)',
type: 'string',
defaultValue: '-',
show (data) {
return !data.allmetrics;
}
},
MetricStatus1: {
ref: 'metricsstatus1',
translation: 'Critic is less than',
type: 'number',
defaultValue: -0.1
},
ColorStatus1: {
ref: 'colorstatus1',
label: 'Critic Color Fill',
type: 'object',
component: 'color-picker',
dualOutput: true,
defaultValue: {
index: 8,
color: '#f93f17'
}
},
ColorStatus1Text: {
ref: 'colorstatus1text',
label: 'Critic Color Text',
type: 'object',
component: 'color-picker',
dualOutput: true,
defaultValue: {
index: 11,
color: '#ffffff'
}
},
MetricStatus2: {
ref: 'metricsstatus2',
translation: 'Medium is less than',
type: 'number',
defaultValue: 0
},
ColorStatus2: {
ref: 'colorstatus2',
label: 'Medium Color Fill',
type: 'object',
component: 'color-picker',
dualOutput: true,
defaultValue: {
index: 9,
color: '#ffcf02'
}
},
ColorStatus2Text: {
ref: 'colorstatus2text',
label: 'Medium Color Text',
type: 'object',
component: 'color-picker',
dualOutput: true,
defaultValue: {
index: 12,
color: '#000000'
}
},
ColorStatus3: {
ref: 'colorstatus3',
label: 'Success Color Fill',
type: 'object',
component: 'color-picker',
dualOutput: true,
defaultValue: {
index: 10,
color: '#276e27'
}
},
ColorStatus3Text: {
ref: 'colorstatus3text',
label: 'Success Color Text',
type: 'object',
component: 'color-picker',
dualOutput: true,
defaultValue: {
index: 11,
color: '#ffffff'
}
}
}
};
export default metricSemaphores;

View File

@@ -6,7 +6,7 @@ const pagination = {
ref: 'maxloops',
type: 'number',
component: 'dropdown',
label: 'Max pagination loops',
label: 'Max Pagination Loops',
options: [
{
value: 1,
@@ -55,7 +55,7 @@ const pagination = {
ref: 'errormessage',
label: 'Default error message',
type: 'string',
defaultValue: 'Unable to display all the data. Apply more filters to limit the amount of displayed data.'
defaultValue: 'Ups! It seems you asked for too many data. Please filter more to see the whole picture.'
}
}
};

View File

@@ -1,53 +1,12 @@
const qlik = window.require('qlik');
// fixes case for when there are 3 dimensions, missies the case with 1 design dimension and 1 data dimension
function hasDesignDimension (data) {
return data.qHyperCubeDef.qDimensions.length > 2;
}
function getFieldList () {
return new Promise(function (resolve) {
const app = qlik.currApp();
app.getList('FieldList').then(function (model) {
// Close the model to prevent any updates.
app.destroySessionObject(model.layout.qInfo.qId);
// This is a bit iffy, might be smarter to reject and handle empty lists on the props instead.
if (!model.layout.qFieldList.qItems) {
return resolve([]);
}
// Resolve an array with master objects.
return resolve(model.layout.qFieldList.qItems.map(function (item) {
return {
value: item.qName,
label: item.qName
};
}));
});
});
}
const tableFormat = {
type: 'items',
label: 'Table format',
label: 'Table Format',
items: {
StylingField: {
ref: 'stylingfield',
disabledRef: '',
type: 'string',
component: 'dropdown',
label: 'Style template field',
options: function () {
return getFieldList().then(function (items) {
items.unshift(
{
value: '',
label: 'None'
});
return items;
});
}
},
IndentBool: {
ref: 'indentbool',
type: 'boolean',
@@ -58,7 +17,7 @@ const tableFormat = {
SeparatorColumns: {
ref: 'separatorcols',
type: 'boolean',
label: 'Column separators',
label: 'Separator Columns',
defaultValue: false
},
rowEvenBGColor: {
@@ -89,7 +48,7 @@ const tableFormat = {
ref: 'BodyTextColorSchema',
type: 'string',
component: 'dropdown',
label: 'Text body color',
label: 'Text Body Color',
options: [
{
value: 'Black',
@@ -139,7 +98,7 @@ const tableFormat = {
ref: 'FontFamily',
type: 'string',
component: 'dropdown',
label: 'Font family',
label: 'FontFamily',
options: [
{
value: 'QlikView Sans, -apple-system, sans-serif',
@@ -174,7 +133,7 @@ const tableFormat = {
},
DataFontSize: {
ref: 'lettersize',
translation: 'Font size',
translation: 'Font Size',
type: 'number',
component: 'buttongroup',
options: [
@@ -191,7 +150,7 @@ const tableFormat = {
},
textAlignment: {
ref: 'cellTextAlignment',
label: 'Cell text alignment',
label: 'Cell Text alignment',
component: 'buttongroup',
options: [
{
@@ -212,7 +171,7 @@ const tableFormat = {
ColumnWidthSlider: {
type: 'number',
component: 'slider',
label: 'Column width',
label: 'Column Width',
ref: 'columnwidthslider',
min: 1,
max: 3,
@@ -246,7 +205,7 @@ const tableFormat = {
ref: 'filteroncellclick',
type: 'boolean',
component: 'switch',
label: 'Allow selection in cells',
label: 'Filter data when cell clicked',
options: [
{
value: true,

View File

@@ -1,32 +1,19 @@
function cleanupNodes (node) {
const removables = node.querySelectorAll('.tooltip,input');
[].forEach.call(removables, removeable => {
if (removeable.parentNode) {
removeable.parentNode.removeChild(removeable);
function removeAllTooltips (node) {
const tooltips = node.querySelectorAll('.tooltip');
[].forEach.call(tooltips, tooltip => {
if (tooltip.parentNode) {
tooltip.parentNode.removeChild(tooltip);
}
});
}
function buildTableHTML (id, title, subtitle, footnote) {
function buildTableHTML (title, subtitle, footnote) {
const titleHTML = `<p style="font-size:15pt"><b>${title}</b></p>`;
const subtitleHTML = `<p style="font-size:11pt">${subtitle}</p>`;
const footnoteHTML = `<p style="font-size:11pt">${footnote}</p>`;
const container = document.querySelector(`[tid="${id}"]`);
const kpiTableClone = container.querySelector('.kpi-table').cloneNode(true);
const dataTableClone = container.querySelector('.data-table').cloneNode(true);
cleanupNodes(kpiTableClone);
cleanupNodes(kpiTableClone);
const footnoteHTML = `<p style="font-size:11pt"><i>Note:</i>${footnote}</p>`;
const dataTableClone = document.querySelector('.data-table').cloneNode(true);
const kpiTableBodies = kpiTableClone.querySelectorAll('tbody');
const dataTableBodies = dataTableClone.querySelectorAll('tbody');
const kpiHeader = kpiTableBodies[0].querySelector('tr');
const dataTableHeaders = dataTableBodies[0].querySelectorAll('tr');
const kpiRows = kpiTableBodies[1].querySelectorAll('tr');
const dataRows = dataTableBodies[1].querySelectorAll('tr');
let combinedRows = '';
for (let i = 0; i < kpiRows.length; i++) {
combinedRows += `<tr>${kpiRows[i].innerHTML}${dataRows[i].innerHTML}</tr>`;
}
removeAllTooltips(dataTableClone);
const tableHTML = `
<html
@@ -54,23 +41,8 @@ function buildTableHTML (id, title, subtitle, footnote) {
<body>
${titleHTML.length > 0 ? titleHTML : ''}
${subtitleHTML.length > 0 ? subtitleHTML : ''}
<div>
<table>
<tbody>
<tr>
${kpiHeader.innerHTML}
${dataTableHeaders[0].innerHTML}
</tr>
${dataTableHeaders.length > 1 ? dataTableHeaders[1].outerHTML : ''}
</tbody>
</table>
<table>
<tbody>
${combinedRows}
</tbody>
</table>
</div>
${footnoteHTML.length > 0 ? footnoteHTML : ''}
${dataTableClone.outerHTML}
</body>
</html>
`.split('>.<')
@@ -83,15 +55,15 @@ function buildTableHTML (id, title, subtitle, footnote) {
function downloadXLS (html) {
const filename = 'analysis.xls';
const blobObject = new Blob([html]);
// 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 = URL.createObjectURL(blobObject);
link.href = dataURI;
link.download = filename;
document.body.appendChild(link);
link.click();
@@ -100,8 +72,15 @@ function downloadXLS (html) {
return true;
}
export function exportXLS (id, title, subtitle, footnote) {
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(id, title, subtitle, footnote);
const table = buildTableHTML(title, subtitle, footnote);
downloadXLS(table);
}

View File

@@ -9,10 +9,10 @@ class ExportButton extends React.PureComponent {
}
handleExport () {
const { id, excelExport, general } = this.props;
const { excelExport, general } = this.props;
const { title, subtitle, footnote } = general;
if (excelExport) {
exportXLS(id, title, subtitle, footnote);
exportXLS(title, subtitle, footnote);
}
}
@@ -34,7 +34,6 @@ ExportButton.defaultProps = {
};
ExportButton.propTypes = {
id: PropTypes.string.isRequired,
excelExport: PropTypes.bool,
general: PropTypes.shape({}).isRequired
};

View File

@@ -1,6 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import { HEADER_FONT_SIZE } from '../initialize-transformed';
import Tooltip from '../tooltip/index.jsx';
class ColumnHeader extends React.PureComponent {
@@ -17,12 +16,11 @@ class ColumnHeader extends React.PureComponent {
render () {
const { baseCSS, cellSuffix, colSpan, entry, styling, qlik } = this.props;
const inEditState = qlik.inEditState();
const isMediumFontSize = styling.headerOptions.fontSizeAdjustment === HEADER_FONT_SIZE.MEDIUM;
const style = {
...baseCSS,
fontSize: `${14 + styling.headerOptions.fontSizeAdjustment}px`,
height: isMediumFontSize ? '43px' : '33px',
height: '45px',
verticalAlign: 'middle'
};
@@ -35,7 +33,6 @@ class ColumnHeader extends React.PureComponent {
>
<Tooltip
isTooltipActive={!inEditState}
styling={styling}
tooltipText={entry.displayValue}
>
{entry.displayValue}

View File

@@ -3,14 +3,14 @@ import PropTypes from 'prop-types';
import ExportButton from '../export-button.jsx';
import { HEADER_FONT_SIZE } from '../initialize-transformed';
const ExportColumnHeader = ({ id, baseCSS, general, title, allowExcelExport, hasSecondDimension, styling }) => {
const ExportColumnHeader = ({ baseCSS, general, title, allowExcelExport, hasSecondDimension, styling }) => {
const rowSpan = hasSecondDimension ? 2 : 1;
const isMediumFontSize = styling.headerOptions.fontSizeAdjustment === HEADER_FONT_SIZE.MEDIUM;
const style = {
...baseCSS,
cursor: 'default',
fontSize: `${16 + styling.headerOptions.fontSizeAdjustment}px`,
height: isMediumFontSize ? '90px' : '70px',
height: isMediumFontSize ? '100px' : '80px',
verticalAlign: 'middle',
width: '230px'
};
@@ -22,7 +22,6 @@ const ExportColumnHeader = ({ id, baseCSS, general, title, allowExcelExport, has
style={style}
>
<ExportButton
id={id}
excelExport={allowExcelExport}
general={general}
/>
@@ -32,7 +31,6 @@ const ExportColumnHeader = ({ id, baseCSS, general, title, allowExcelExport, has
};
ExportColumnHeader.propTypes = {
id: PropTypes.string.isRequired,
allowExcelExport: PropTypes.bool.isRequired,
baseCSS: PropTypes.shape({}).isRequired,
general: PropTypes.shape({}).isRequired,

View File

@@ -5,7 +5,7 @@ import ColumnHeader from './column-header.jsx';
import MeasurementColumnHeader from './measurement-column-header.jsx';
import { injectSeparators } from '../utilities';
const HeadersTable = ({ data, general, qlik, styling, isKpi }) => {
const HeadersTable = ({ data, general, qlik, styling }) => {
const baseCSS = {
backgroundColor: styling.headerOptions.colorSchema,
color: styling.headerOptions.textColor,
@@ -26,18 +26,15 @@ const HeadersTable = ({ data, general, qlik, styling, isKpi }) => {
<table className="header">
<tbody>
<tr>
{isKpi ?
<ExportColumnHeader
id={qlik.options.id}
allowExcelExport={general.allowExcelExport}
baseCSS={baseCSS}
general={general}
hasSecondDimension={hasSecondDimension}
styling={styling}
title={dimension1[0].name}
/> : null
}
{!isKpi && !hasSecondDimension && measurements.map(measurementEntry => (
<ExportColumnHeader
allowExcelExport={general.allowExcelExport}
baseCSS={baseCSS}
general={general}
hasSecondDimension={hasSecondDimension}
styling={styling}
title={dimension1[0].name}
/>
{!hasSecondDimension && measurements.map(measurementEntry => (
<MeasurementColumnHeader
baseCSS={baseCSS}
general={general}
@@ -47,7 +44,7 @@ const HeadersTable = ({ data, general, qlik, styling, isKpi }) => {
styling={styling}
/>
))}
{!isKpi && hasSecondDimension && injectSeparators(dimension2, styling.useSeparatorColumns).map((entry, index) => {
{hasSecondDimension && injectSeparators(dimension2, styling.useSeparatorColumns).map((entry, index) => {
if (entry.isSeparator) {
const separatorStyle = {
color: 'white',
@@ -78,7 +75,7 @@ const HeadersTable = ({ data, general, qlik, styling, isKpi }) => {
);
})}
</tr>
{!isKpi && hasSecondDimension && (
{hasSecondDimension && (
<tr>
{injectSeparators(dimension2, styling.useSeparatorColumns).map((dimensionEntry, index) => {
if (dimensionEntry.isSeparator) {
@@ -140,8 +137,7 @@ HeadersTable.propTypes = {
styling: PropTypes.shape({
headerOptions: PropTypes.shape({}),
options: PropTypes.shape({})
}).isRequired,
isKpi: PropTypes.bool.isRequired
}).isRequired
};
export default HeadersTable;

View File

@@ -18,8 +18,9 @@ const MeasurementColumnHeader = ({ baseCSS, general, hasSecondDimension, measure
}
const cellStyle = {
...baseCSS,
cursor: 'default',
fontSize: `${baseFontSize + fontSizeAdjustment}px`,
height: isMediumFontSize ? '45px' : '35px',
height: isMediumFontSize ? '40px' : '25px',
verticalAlign: 'middle'
};
return (
@@ -27,20 +28,25 @@ const MeasurementColumnHeader = ({ baseCSS, general, hasSecondDimension, measure
className={`${cellClass}${general.cellSuffix}`}
style={cellStyle}
>
<Tooltip
tooltipText={title}
styling={styling}
>
{title}
</Tooltip>
<span className="wrapclass25">
<Tooltip
tooltipText={title}
>
{title}
</Tooltip>
</span>
</th>
);
}
const isLong = (title.length > 11 && fontSizeAdjustment === HEADER_FONT_SIZE.SMALL)
|| (title.length > 12 && fontSizeAdjustment === HEADER_FONT_SIZE.MEDIUM);
const suffixWrap = isLong ? '70' : 'empty';
const style = {
...baseCSS,
cursor: 'default',
fontSize: `${15 + fontSizeAdjustment}px`,
height: isMediumFontSize ? '90px' : '70px',
height: isMediumFontSize ? '100px' : '80px',
verticalAlign: 'middle'
};
return (
@@ -48,12 +54,12 @@ const MeasurementColumnHeader = ({ baseCSS, general, hasSecondDimension, measure
className={`grid-cells2${general.cellSuffix}`}
style={style}
>
<Tooltip
tooltipText={title}
styling={styling}
<span
className={`wrapclass${suffixWrap}`}
style={{ fontFamily: styling.headerOptions.fontFamily }}
>
{title}
</Tooltip>
</span>
</th>
);
};

View File

@@ -1,9 +1,5 @@
import paint from './paint.jsx';
import definition from './definition';
import { initializeDataCube, initializeDesignList } from './dataset';
import initializeStore from './store';
import React from 'react';
import ReactDOM from 'react-dom';
import Root from './root.jsx';
import './main.less';
if (!window._babelPolyfill) { // eslint-disable-line no-underscore-dangle
@@ -24,16 +20,12 @@ export default {
},
data: {
dimensions: {
max: function (nMeasures) {
return nMeasures < 9 ? 2 : 1;
},
max: 3,
min: 1,
uses: 'dimensions'
},
measures: {
max: function (nDims) {
return nDims < 2 ? 9 : 8;
},
max: 8,
min: 1,
uses: 'measures'
}
@@ -57,34 +49,16 @@ export default {
exportData: true,
snapshot: true
},
paint: async function ($element, layout) {
const dataCube = await initializeDataCube(this, layout);
const designList = await initializeDesignList(this, layout);
const state = await initializeStore({
$element,
component: this,
dataCube,
designList,
layout
});
const editmodeClass = this.inAnalysisState() ? '' : 'edit-mode';
const jsx = (
<Root
editmodeClass={editmodeClass}
qlik={this}
state={state}
/>
);
ReactDOM.render(jsx, $element[0]);
paint ($element, layout) {
try {
paint($element, layout, this);
} catch (exception) {
console.error(exception); // eslint-disable-line no-console
throw exception;
}
},
snapshot: {
canTakeSnapshot: true
},
setSnapshotData: async function (snapshotLayout) {
snapshotLayout.snapshotData.dataCube = await initializeDataCube(this, snapshotLayout);
snapshotLayout.snapshotData.designList = await initializeDesignList(this, snapshotLayout);
return snapshotLayout;
},
version: 1.0
};

View File

@@ -67,7 +67,7 @@ function generateMeasurements (information) {
function generateDimensionEntry (information, data) {
return {
displayValue: data.qText || data.qNum,
displayValue: data.qText,
elementNumber: data.qElemNumber,
name: information.qFallbackTitle,
value: data.qNum
@@ -90,8 +90,7 @@ function generateMatrixCell ({ cell, dimension1Information, dimension2Informatio
header: dimension1Information.qText
},
measurement: {
header: measurementInformation.name,
index: measurementInformation.index
header: measurementInformation.name
}
},
value: cell.qNum
@@ -107,16 +106,15 @@ function generateMatrixCell ({ cell, dimension1Information, dimension2Informatio
}
let lastRow = 0;
function generateDataSet (
component, dimensionsInformation, measurementsInformation, dataCube) {
const measurements = generateMeasurements(measurementsInformation);
function generateDataSet (component, dimensionsInformation, measurementsInformation, cubes) {
let dimension1 = [];
let dimension2 = [];
const measurements = generateMeasurements(measurementsInformation);
let matrix = [];
const hasSecondDimension = dimensionsInformation.length > 1;
dataCube.forEach(row => {
const hasDesignDimension = cubes.design;
const hasSecondDimension = hasDesignDimension ? dimensionsInformation.length > 2 : dimensionsInformation.length > 1;
cubes.data.forEach(row => {
lastRow += 1;
const dimension1Entry = generateDimensionEntry(dimensionsInformation[0], row[0]);
dimension1.push(dimension1Entry);
@@ -131,7 +129,6 @@ function generateDataSet (
.slice(firstDataCell, row.length)
.map((cell, cellIndex) => {
const measurementInformation = measurements[cellIndex];
measurementInformation.index = cellIndex;
const dimension1Information = row[0]; // eslint-disable-line prefer-destructuring
const dimension2Information = hasSecondDimension ? row[1] : null;
const generatedCell = generateMatrixCell({
@@ -163,24 +160,33 @@ function generateDataSet (
// Make sure all rows are saturated, otherwise data risks being displayed in the wrong column
matrix = matrix.map((row, rowIndex) => {
if ((hasSecondDimension && row.length == (dimension2.length * measurements.length))
|| (!hasSecondDimension && row.length == measurements.length)) {
if (row.length == dimension2.length) {
// Row is saturated
return row;
}
// Row is not saturated, so must add empty cells to fill the gaps
let newRow = [];
if (hasSecondDimension) {
// Got a second dimension, so need to add measurements for all values of the second dimension
let rowDataIndex = 0;
dimension2.forEach(dim => {
rowDataIndex = appendMissingCells(
row, newRow, rowDataIndex, measurements, rowIndex, dim.elementNumber);
let cellIndex = 0;
dimension2.forEach(dim => {
measurements.forEach(measurement => {
if (cellIndex < row.length
&& row[cellIndex].parents.dimension2.elementNumber === dim.elementNumber
&& row[cellIndex].parents.measurement.header === measurement.name) {
newRow.push(row[cellIndex]);
cellIndex++;
} else {
newRow.push({
displayValue: '',
parents: {
dimension1: { elementNumber: rowIndex },
dimension2: { elementNumber: dim.elementNumber },
measurement: { header: measurement.name }
}
});
}
});
} else {
appendMissingCells(row, newRow, 0, measurements, rowIndex);
}
});
return newRow;
});
@@ -193,43 +199,7 @@ function generateDataSet (
};
}
/*
* Appends the cells of the source row, as well as those missing, to the destination row, starting
* from the given source index. Returns the source index of the next source cell after this has
* completed. If there is a second dimension the dim2ElementNumber should be set to the current
* index of the dimension2 value being processed.
*/
function appendMissingCells (
sourceRow, destRow, sourceIndex, measurements, dim1ElementNumber, dim2ElementNumber = -1) {
let index = sourceIndex;
measurements.forEach((measurement, measureIndex) => {
if (index < sourceRow.length
&& (dim2ElementNumber === -1
|| sourceRow[index].parents.dimension2.elementNumber === dim2ElementNumber)
&& sourceRow[index].parents.measurement.header === measurement.name) {
// Source contains the expected cell
destRow.push(sourceRow[index]);
index++;
} else {
// Source doesn't contain the expected cell, so add empty
destRow.push({
displayValue: '',
parents: {
dimension1: { elementNumber: dim1ElementNumber },
dimension2: { elementNumber: dim2ElementNumber },
measurement: {
header: measurement.name,
index: measureIndex
}
}
});
}
});
return index;
}
function initializeTransformed ({ $element, component, dataCube, designList, layout }) {
function initializeTransformed ({ $element, component, cubes, layout }) {
const dimensionsInformation = component.backendApi.getDimensionInfos();
const measurementsInformation = component.backendApi.getMeasureInfos();
const dimensionCount = layout.qHyperCube.qDimensionInfo.length;
@@ -240,18 +210,19 @@ function initializeTransformed ({ $element, component, dataCube, designList, lay
dimension2,
measurements,
matrix
} = generateDataSet(component, dimensionsInformation, measurementsInformation, dataCube);
} = generateDataSet(component, dimensionsInformation, measurementsInformation, cubes);
const customSchemaBasic = [];
const customSchemaFull = [];
let customHeadersCount = 0;
if (designList && designList.length > 0) {
const headers = designList[0].split(';');
if (cubes.design) {
const allTextLines = cubes.design.map(entry => entry[0].qText);
const headers = allTextLines[0].split(';');
customHeadersCount = headers.length;
for (let lineNumber = 0; lineNumber < designList.length; lineNumber += 1) {
for (let lineNumber = 0; lineNumber < allTextLines.length; lineNumber += 1) {
customSchemaFull[lineNumber] = new Array(headers.length);
const data = designList[lineNumber].split(';');
const data = allTextLines[lineNumber].split(';');
if (data.length === headers.length) {
for (let headerIndex = 0; headerIndex < headers.length; headerIndex += 1) {
@@ -295,7 +266,7 @@ function initializeTransformed ({ $element, component, dataCube, designList, lay
count: customHeadersCount,
full: customSchemaFull
},
hasCustomFileStyle: Boolean(designList),
hasCustomFileStyle: Boolean(cubes.design),
headerOptions: {
alignment: getAlignment(layout.HeaderAlign),
colorSchema: layout.HeaderColorSchema.color,
@@ -310,30 +281,39 @@ function initializeTransformed ({ $element, component, dataCube, designList, lay
fontSizeAdjustment: getFontSizeAdjustment(layout.lettersize),
textAlignment: layout.cellTextAlignment
},
conditionalColoring: {
enabled: layout.conditionalcoloring.enabled,
colorAllRows: layout.conditionalcoloring.colorall,
rows: layout.conditionalcoloring.rows.map(row => row.rowname),
colorAllMeasures: typeof layout.conditionalcoloring.colorallmeasures === 'undefined'
|| layout.conditionalcoloring.colorallmeasures,
measures: !layout.conditionalcoloring.measures
? [] : layout.conditionalcoloring.measures.split(',').map(index => Number(index)),
threshold: {
poor: layout.conditionalcoloring.threshold_poor,
fair: layout.conditionalcoloring.threshold_fair
semaphoreColors: {
fieldsToApplyTo: {
applyToAll: layout.allsemaphores,
applyToMetric: layout.allmetrics,
specificFields: [
layout.conceptsemaphore1,
layout.conceptsemaphore2,
layout.conceptsemaphore3,
layout.conceptsemaphore4,
layout.conceptsemaphore5,
layout.conceptsemaphore6,
layout.conceptsemaphore7,
layout.conceptsemaphore9,
layout.conceptsemaphore10
],
metricsSpecificFields: layout.metricssemaphore.split(',').map(entry => Number(entry))
},
colors: {
poor: {
color: layout.conditionalcoloring.color_poor,
textColor: layout.conditionalcoloring.textcolor_poor
status: {
critical: layout.metricsstatus1,
medium: layout.metricsstatus2
},
statusColors: {
critical: {
backgroundColor: layout.colorstatus1.color,
color: layout.colorstatus1text.color
},
fair: {
color: layout.conditionalcoloring.color_fair,
textColor: layout.conditionalcoloring.textcolor_fair
medium: {
backgroundColor: layout.colorstatus2.color,
color: layout.colorstatus2text.color
},
good: {
color: layout.conditionalcoloring.color_good,
textColor: layout.conditionalcoloring.textcolor_good
normal: {
backgroundColor: layout.colorstatus3.color,
color: layout.colorstatus3text.color
}
}
},

View File

@@ -1,5 +1,6 @@
/* eslint-disable */
.qv-object-qlik-smart-pivot {
@TableBorder: 1px solid #d3d3d3;
@KpiTableWidth: 230px;
*,
@@ -32,17 +33,18 @@
}
table {
border-collapse: separate;
border-spacing: 1px;
border-collapse: collapse;
border-spacing: 0;
width: auto;
border-left: @TableBorder;
border-right: @TableBorder;
border-top: @TableBorder;
}
tr {
height: 25px;
}
td,
th {
border: 1px solid #fff;
border-collapse: collapse;
padding: 5px !important; // prevent overwriting from single object
white-space: nowrap;
overflow: hidden;
@@ -68,12 +70,37 @@
th.main-kpi {
text-align: center;
vertical-align: middle;
border-bottom: @TableBorder;
}
.numeric {
text-align: right;
}
// This is for wrap text in headers
.wrapclass25 {
width: 100%;
height: inherit;
white-space: pre-line;
overflow: hidden;
display: block;
text-overflow: ellipsis;
}
.wrapclass70 {
width: 100%;
height: 70px;
white-space: pre-line;
overflow: hidden;
display: inline-block;
vertical-align: middle;
line-height: 20px;
}
.wrapclassEmpty {
width: 100%;
}
// *****************
// Medium column size
// *****************
@@ -143,7 +170,12 @@
background-color: #fff;
}
tbody tr:hover td {
.fdim-cells:hover {
background-color: #808080 !important;
color: #fff;
}
tbody tr:hover {
cursor: default;
background-color: #808080 !important;
color: #fff;
@@ -170,50 +202,33 @@
border: groove;
}
.root {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
height: 100%;
width: 100%;
}
.kpi-table .fdim-cells,
.data-table td {
line-height: 1em !important;
}
.data-table .fdim-cells {
display: none;
}
.kpi-table {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
flex: none;
width: @KpiTableWidth !important;
overflow: hidden !important;
height: 100%;
margin: 0;
padding: 0;
.header-wrapper {
flex: none;
box-shadow: 4px 2px 8px #e1e1e1;
}
position: absolute;
top: 0;
left: 0;
border-right: 1px solid #fff;
box-shadow: 4px 2px 8px #e1e1e1;
.row-wrapper {
height: calc(~"100% - 97px");
overflow: scroll;
margin: 0;
margin-bottom: 8px;
position: absolute;
padding: 0;
box-shadow: 4px 2px 8px #e1e1e1;
min-height: 0; /* This is to make flex size-filling work */
/* Adapt for Edge */
@supports (-ms-ime-align: auto) {
margin-bottom: 16px;
}
/* Adapt for IE11 */
@media screen and (-ms-high-contrast: none) {
margin-bottom: 16px;
}
margin-top: 0;
}
}
@@ -222,50 +237,27 @@
}
.data-table {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
margin-left: 13px;
min-width: 0; /* This is to make flex size-filling work */
height: 100%;
width: calc(100% - 243px);
position: absolute;
margin-left: @KpiTableWidth + 13px;
.header-wrapper {
flex: none;
overflow: scroll;
margin-right: 8px;
width: 100%;
}
.row-wrapper {
overflow: scroll;
margin: 0;
padding: 0;
min-height: 0; /* This is to make flex size-filling work */
/* Style scrollbar for FF */
scrollbar-width: thin;
scrollbar-color: #d3d3d3 transparent;
}
/* Adapt for Edge */
@supports (-ms-ime-align: auto) {
.header-wrapper {
margin-right: 16px;
}
}
/* Adapt for IE11 */
@media screen and (-ms-high-contrast: none) {
height: calc(~"100% - 97px");
width: 100%;
height: 100%;
.header-wrapper {
margin-right: 16px;
}
overflow: scroll;
padding: 0;
margin-top: 0;
}
}
// hide scrollbars
.kpi-table .header-wrapper,
.kpi-table .row-wrapper,
.data-table .header-wrapper {
// stylelint-disable-next-line property-no-unknown
scrollbar-width: none;

28
src/paint.jsx Normal file
View File

@@ -0,0 +1,28 @@
import initializeStore from './store';
import React from 'react';
import ReactDOM from 'react-dom';
import Root from './root.jsx';
import { initializeCubes } from './dataset';
export default async function paint ($element, layout, component) {
const cubes = await initializeCubes({
component,
layout
});
const state = await initializeStore({
$element,
component,
cubes,
layout
});
const editmodeClass = component.inAnalysisState() ? '' : 'edit-mode';
const jsx = (
<Root
qlik={component}
state={state}
editmodeClass={editmodeClass}
/>
);
ReactDOM.render(jsx, $element[0]);
}

View File

@@ -5,50 +5,46 @@ import DataTable from './data-table/index.jsx';
import { LinkedScrollWrapper, LinkedScrollSection } from './linked-scroll';
const Root = ({ state, qlik, editmodeClass }) => (
<div className="root">
<LinkedScrollWrapper>
<div className={`kpi-table ${editmodeClass}`}>
<LinkedScrollWrapper>
<div className={`kpi-table ${editmodeClass}`}>
<HeadersTable
data={state.data}
general={state.general}
qlik={qlik}
styling={state.styling}
/>
<LinkedScrollSection linkVertical>
<DataTable
data={state.data}
general={state.general}
qlik={qlik}
renderData={false}
styling={state.styling}
/>
</LinkedScrollSection>
</div>
<div className={`data-table ${editmodeClass}`}>
<LinkedScrollSection linkHorizontal>
<HeadersTable
data={state.data}
general={state.general}
isKpi
qlik={qlik}
styling={state.styling}
/>
<LinkedScrollSection linkVertical>
<DataTable
data={state.data}
general={state.general}
qlik={qlik}
renderData={false}
styling={state.styling}
/>
</LinkedScrollSection>
</div>
<div className={`data-table ${editmodeClass}`}>
<LinkedScrollSection linkHorizontal>
<HeadersTable
data={state.data}
general={state.general}
isKpi={false}
qlik={qlik}
styling={state.styling}
/>
</LinkedScrollSection>
<LinkedScrollSection
linkHorizontal
linkVertical
>
<DataTable
data={state.data}
general={state.general}
qlik={qlik}
styling={state.styling}
/>
</LinkedScrollSection>
</div>
</LinkedScrollWrapper>
</div>
</LinkedScrollSection>
<LinkedScrollSection
linkHorizontal
linkVertical
>
<DataTable
data={state.data}
general={state.general}
qlik={qlik}
styling={state.styling}
/>
</LinkedScrollSection>
</div>
</LinkedScrollWrapper>
);
Root.propTypes = {

View File

@@ -1,11 +1,10 @@
import initializeTransformed from './initialize-transformed';
async function initialize ({ $element, layout, component, dataCube, designList }) {
async function initialize ({ $element, layout, component, cubes }) {
const transformedProperties = await initializeTransformed({
$element,
component,
dataCube,
designList,
cubes,
layout
});

View File

@@ -25,21 +25,23 @@ class Tooltip extends React.PureComponent {
}
render () {
const { children, styling, tooltipText } = this.props;
const { children, tooltipText } = this.props;
const { showTooltip } = this.state;
return (
<div
onMouseMove={handleCalculateTooltipPosition}
onMouseOut={this.handleRenderTooltip}
onMouseOver={this.handleRenderTooltip}
style={{ fontFamily: styling.options.fontFamily }}
>
{children}
{showTooltip
? (
<div className="tooltip-wrapper">
<p style={{ fontFamily: styling.options.fontFamily }}>
<div
className="tooltip-wrapper"
>
<p>
{tooltipText}
</p>
</div>
@@ -54,11 +56,6 @@ Tooltip.propTypes = {
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired,
styling: PropTypes.shape({
options: PropTypes.shape({
fontFamily: PropTypes.string.isRequired
}).isRequired
}).isRequired,
tooltipText: PropTypes.string.isRequired
};

View File

@@ -84,7 +84,7 @@ module.exports = {
'indentation': 2,
'length-zero-no-unit': true,
'max-empty-lines': 1,
'max-nesting-depth': 5,
'max-nesting-depth': 3,
'media-feature-colon-space-after': 'always',
'media-feature-colon-space-before': 'never',
'media-feature-name-case': 'lower',