UNPKG

@gooddata/react-components

Version:

GoodData.UI - A powerful JavaScript library for building analytical applications

290 lines (248 loc) • 10.5 kB
// (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; } };