@gooddata/react-components
Version:
GoodData.UI - A powerful JavaScript library for building analytical applications
290 lines (248 loc) • 10.5 kB
text/typescript
// (C) 2007-2020 GoodData Corporation
import { ICellRendererParams, ColDef, Column } from "ag-grid-community";
import omit = require("lodash/omit");
import escape = require("lodash/escape");
import pickBy = require("lodash/pickBy");
import size = require("lodash/size");
import stringify = require("json-stable-stringify");
import invariant = require("invariant");
import { AFM, Execution } from "@gooddata/typings";
import { getMappingHeaderUri } from "../../../helpers/mappingHeader";
import {
IMappingHeader,
isMappingHeaderTotal,
isMappingHeaderMeasureItem,
} from "../../../interfaces/MappingHeader";
import {
DOT_PLACEHOLDER,
FIELD_SEPARATOR,
FIELD_SEPARATOR_PLACEHOLDER,
ID_SEPARATOR,
ID_SEPARATOR_PLACEHOLDER,
ROW_TOTAL,
ROW_SUBTOTAL,
MEASURE_COLUMN,
} from "./agGridConst";
import { IGridHeader } from "./agGridTypes";
/*
* Assorted utility functions used in our Pivot Table -> ag-grid integration.
*/
export const sanitizeField = (field: string) =>
// Identifiers can not contain a dot character, because AGGrid cannot handle it.
// Alternatively, we could handle it with a custom renderer (works in RowLoadingElement).
field
.replace(/\./g, DOT_PLACEHOLDER)
.replace(new RegExp(FIELD_SEPARATOR, "g"), FIELD_SEPARATOR_PLACEHOLDER)
.replace(new RegExp(ID_SEPARATOR, "g"), ID_SEPARATOR_PLACEHOLDER);
// returns [attributeId, attributeValueId]
// attributeValueId can be null if supplied with attribute uri instead of attribute value uri
export const getIdsFromUri = (uri: string, sanitize = true) => {
const [, attributeId, , attributeValueId = null] = uri.match(/obj\/([^\/]*)(\/elements\?id=)?(.*)?$/);
return [attributeId, attributeValueId].map((id: string | null) =>
id && sanitize ? sanitizeField(id) : id,
);
};
export const getParsedFields = (colId: string): string[][] => {
// supported colIds are 'a_2009', 'a_2009_4-a_2071_12', 'a_2009_4-a_2071_12-m_3'
return colId.split(FIELD_SEPARATOR).map((field: string) => field.split(ID_SEPARATOR));
};
export const colIdIsSimpleAttribute = (colId: string) => {
const parsedFields = getParsedFields(colId);
return parsedFields[0].length === 2 && parsedFields[0][0] === "a";
};
export const getRowNodeId = (item: any) => {
return Object.keys(item.headerItemMap)
.map(key => {
const mappingHeader: IMappingHeader = item.headerItemMap[key];
if (isMappingHeaderTotal(mappingHeader)) {
return `${key}${ID_SEPARATOR}${mappingHeader.totalHeaderItem.name}`;
}
const uri = getMappingHeaderUri(mappingHeader);
const ids = getIdsFromUri(uri);
return `${key}${ID_SEPARATOR}${ids[1]}`;
})
.join(FIELD_SEPARATOR);
};
export const getGridIndex = (position: number, gridDistance: number) => {
return Math.floor(position / gridDistance);
};
export const cellRenderer = (params: ICellRendererParams) => {
const isRowTotalOrSubtotal =
params.data &&
params.data.type &&
(params.data.type === ROW_TOTAL || params.data.type === ROW_SUBTOTAL);
const isActiveRowTotal =
isRowTotalOrSubtotal && // short circuit for non row totals
params.data &&
params.data.rowTotalActiveMeasures &&
params.data.rowTotalActiveMeasures.some((measureColId: string) =>
params.colDef.field.endsWith(measureColId),
);
const formattedValue =
isRowTotalOrSubtotal && !isActiveRowTotal && !params.value
? "" // inactive row total cells should be really empty (no "-") when they have no value (RAIL-1525)
: escape(params.formatValue(params.value));
const className = params.node.rowPinned === "top" ? "gd-sticky-header-value" : "s-value";
return `<span class="${className}">${formattedValue || ""}</span>`;
};
export const getTreeLeaves = (tree: any, getChildren = (node: any) => node && node.children) => {
const leaves = [];
const nodes = Array.isArray(tree) ? [...tree] : [tree];
let node;
let children;
while (
// tslint:disable:no-conditional-assignment ban-comma-operator
((node = nodes.shift()),
(children = getChildren(node)),
(children && children.length) || (leaves.push(node) && nodes.length))
// tslint:enable:no-conditional-assignment ban-comma-operator
) {
if (children) {
nodes.push(...children);
}
}
return leaves;
};
export const indexOfTreeNode = (
node: any,
tree: any,
matchNode = (nodeA: any, nodeB: any) => nodeA === nodeB,
getChildren = (node: any) => (node && node.children) || [],
indexes: number[] = [],
): number[] => {
const nodes = Array.isArray(tree) ? [...tree] : [tree];
for (let nodeIndex = 0; nodeIndex < nodes.length; nodeIndex++) {
const currentNode = nodes[nodeIndex];
// match current node
if (matchNode(currentNode, node)) {
return [...indexes, nodeIndex];
}
// check children
const childrenMatchIndexes = indexOfTreeNode(node, getChildren(currentNode), matchNode, getChildren, [
...indexes,
nodeIndex,
]);
if (childrenMatchIndexes !== null) {
return childrenMatchIndexes;
}
}
return null;
};
export function isMeasureColumnReadyToRender(params: any, execution: Execution.IExecutionResponses): boolean {
return Boolean(params && params.value !== undefined && execution && execution.executionResponse);
}
export function getMeasureFormat(gridHeader: IGridHeader, execution: Execution.IExecutionResponses): string {
const headers = execution.executionResponse.dimensions[1].headers;
const header = headers[headers.length - 1];
if (!Execution.isMeasureGroupHeader(header)) {
throw new Error(`Cannot get measure format from header ${Object.keys(header)}`);
}
const measureIndex = gridHeader.measureIndex;
return header.measureGroupHeader.items[measureIndex].measureHeaderItem.format;
}
export function getSubtotalStyles(dimension: AFM.IDimension): string[] {
if (!dimension || !dimension.totals) {
return [];
}
let even = false;
const subtotalStyles = dimension.itemIdentifiers.slice(1).map(attributeIdentifier => {
const hasSubtotal = dimension.totals.some(total => total.attributeIdentifier === attributeIdentifier);
if (hasSubtotal) {
even = !even;
return even ? "even" : "odd";
}
return null;
});
// Grand total (first) has no styles
return [null, ...subtotalStyles];
}
export function generateAgGridComponentKey(afm: AFM.IAfm, rendererId: number): string {
const afmWithoutTotals: Partial<AFM.IAfm> = omit<AFM.IAfm>(afm, ["nativeTotals"]);
return `agGridKey-${stringify(afmWithoutTotals)}-${rendererId}`;
}
export function getLastFieldType(fields: string[][]): string {
const [lastFieldType] = fields[fields.length - 1];
return lastFieldType;
}
export function getLastFieldId(fields: string[][]): string {
const [, lastFieldId] = fields[fields.length - 1];
return lastFieldId;
}
export function getAttributeLocators(fields: string[][], attributeHeaders: Execution.IAttributeHeader[]) {
return fields.slice(0, -1).map((field: string[]) => {
// first item is type which should be always 'a'
const [, fieldId, fieldValueId] = field;
const attributeHeaderMatch = attributeHeaders.find((attributeHeader: Execution.IAttributeHeader) => {
return getIdsFromUri(attributeHeader.attributeHeader.formOf.uri)[0] === fieldId;
});
invariant(
attributeHeaderMatch,
`Could not find matching attribute header to field ${field.join(ID_SEPARATOR)}`,
);
return {
attributeLocatorItem: {
attributeIdentifier: attributeHeaderMatch.attributeHeader.localIdentifier,
element: `${attributeHeaderMatch.attributeHeader.formOf.uri}/elements?id=${fieldValueId}`,
},
};
});
}
export const getColumnIdentifierFromDef = (colDef: IGridHeader | ColDef): string => {
// field should be always present, fallback to colId could happen for empty columns
return colDef.field || colDef.colId;
};
export const getColumnIdentifier = (item: Column | IGridHeader | ColDef): string => {
if (isColumn(item)) {
return getColumnIdentifierFromDef(item.getColDef());
}
return getColumnIdentifierFromDef(item);
};
export function isColumn(item: Column | ColDef): item is Column {
return !!(item as Column).getColDef;
}
export const isMeasureColumn = (item: Column | ColDef) => {
if (isColumn(item)) {
return item.getColDef().type === MEASURE_COLUMN;
}
return item.type === MEASURE_COLUMN;
};
// ONE-4508 (bugfix) - in AFM object are presents empty arrays and therefore is necessary to sanitize
export const sanitizeFingerprint = (fingerprint: string): string => {
let parsedFingerprint;
try {
parsedFingerprint = JSON.parse(fingerprint);
} catch {
console.error("unable to parse fingerprint"); // tslint:disable-line
}
if (!parsedFingerprint) {
return fingerprint;
}
const parsedFingerprintAfm = {
...parsedFingerprint.afm,
};
const sanitizedParsedFingerprintAfm = pickBy(parsedFingerprintAfm, size);
return JSON.stringify({
...parsedFingerprint,
afm: sanitizedParsedFingerprintAfm,
});
};
export const isColumnDisplayed = (displayedColumns: Column[], column: Column) => {
return displayedColumns.some(displayedColumn => displayedColumn.getColId() === column.getColId());
};
const getMappingHeaderMeasureItem = (item: Column | ColDef): Execution.IMeasureHeaderItem | undefined => {
if (!isMeasureColumn(item)) {
return;
}
const headers: IMappingHeader[] = isColumn(item)
? (item.getColDef() as IGridHeader).drillItems
: (item as IGridHeader).drillItems;
if (headers) {
return headers.filter(isMappingHeaderMeasureItem)[0];
}
};
export const getMappingHeaderMeasureItemLocalIdentifier = (item: Column | ColDef): string | undefined => {
const measure = getMappingHeaderMeasureItem(item);
if (measure) {
return measure.measureHeaderItem.localIdentifier;
}
};