UNPKG

@gooddata/react-components

Version:

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

306 lines (252 loc) • 9.56 kB
// (C) 2007-2021 GoodData Corporation import remove = require("lodash/remove"); import cloneDeep = require("lodash/cloneDeep"); import sortedUniq = require("lodash/sortedUniq"); import clone = require("lodash/clone"); import without = require("lodash/without"); import omit = require("lodash/omit"); import sortBy = require("lodash/sortBy"); import get = require("lodash/get"); import { AFM } from "@gooddata/typings"; import { IntlShape } from "react-intl"; import { isMappingHeaderAttribute, isMappingHeaderMeasureItem, IMappingHeader, } from "../../../../interfaces/MappingHeader"; import { IIndexedTotalItem, ITotalWithData } from "../../../../interfaces/Totals"; import { IAlignPoint, ITotalsDataSource, ITotalTypeWithTitle } from "../../../../interfaces/Table"; import { getFooterHeight } from "../utils/layout"; export const AVAILABLE_TOTALS: AFM.TotalType[] = ["sum", "max", "min", "avg", "med", "nat"]; export const isNativeTotal = (total: AFM.ITotalItem) => { return total && total.type === "nat"; }; export const getAttributeDimension = ( attributeIdentifier: string, resultSpec: AFM.IResultSpec, ): AFM.IDimension => { return resultSpec.dimensions.find( dimension => !!dimension.itemIdentifiers.find(attribute => attribute === attributeIdentifier), ); }; const getNativeTotalAttributeIdentifiers = (total: AFM.ITotalItem, resultSpec: AFM.IResultSpec): string[] => { const attributeIdentifiers = getAttributeDimension(total.attributeIdentifier, resultSpec).itemIdentifiers; const totalAttributeIndex = attributeIdentifiers.findIndex( attributeIdentifier => attributeIdentifier === total.attributeIdentifier, ); return attributeIdentifiers.slice(0, totalAttributeIndex); }; export const getNativeTotals = ( totals: AFM.ITotalItem[], resultSpec: AFM.IResultSpec, ): AFM.INativeTotalItem[] => { if (!totals) { return []; } const afmNativeTotals: AFM.INativeTotalItem[] = totals .filter(total => isNativeTotal(total)) .map(nativeTotal => ({ measureIdentifier: nativeTotal.measureIdentifier, attributeIdentifiers: getNativeTotalAttributeIdentifiers(nativeTotal, resultSpec), })); return afmNativeTotals; }; export const getTotalsFromResultSpec = (resultSpec: AFM.IResultSpec): AFM.ITotalItem[] => { return resultSpec && resultSpec.dimensions ? resultSpec.dimensions.reduce( (totals: AFM.ITotalItem[], dimension) => dimension && dimension.totals ? totals.concat(dimension.totals) : totals, [], ) : []; }; function getTotalsList(intl: IntlShape): ITotalTypeWithTitle[] { return AVAILABLE_TOTALS.map(type => ({ type, title: intl.formatMessage({ id: `visualizations.totals.dropdown.title.${type}` }), })); } export function getTotalsDataSource(usedTotals: ITotalWithData[], intl: IntlShape): ITotalsDataSource { const usedTotalsTypes: AFM.TotalType[] = usedTotals.map((total: ITotalWithData) => total.type); const list: ITotalTypeWithTitle[] = getTotalsList(intl).map((total: ITotalTypeWithTitle) => ({ ...total, disabled: usedTotalsTypes.includes(total.type), })); list.unshift({ title: "visualizations.totals.dropdown.heading", role: "header", }); return { rowsCount: list.length, getObjectAt: (index: number) => list[index], }; } export function createTotalItem( type: AFM.TotalType, outputMeasureIndexes: number[] = [], values: number[] = [], ): ITotalWithData { return { type, outputMeasureIndexes, values, }; } export function orderTotals(totalsUnordered: IIndexedTotalItem[]): IIndexedTotalItem[] { return sortBy(totalsUnordered, total => AVAILABLE_TOTALS.indexOf(total.type)); } export function toggleCellClass( parentReference: Element, tableColumnIndex: number, isHighlighted: boolean, className: string, ): void { const cells: NodeListOf<Element> = parentReference.querySelectorAll(`.col-${tableColumnIndex}`); Array.from(cells).forEach((cell: Element) => { if (isHighlighted) { cell.classList.add(className); } else { cell.classList.remove(className); } }); } export function resetRowClass( parentReference: Element, className: string, selector: string, rowIndexToBeSet: number = null, ): void { const rows: NodeListOf<Element> = parentReference.querySelectorAll(selector); Array.from(rows).forEach((r: Element) => r.classList.remove(className)); if (rows.length && rowIndexToBeSet !== null) { const row: Element = rows[rowIndexToBeSet]; row.classList.add(className); } } export function removeTotalsRow( totals: ITotalWithData[], totalItemTypeToRemove: AFM.TotalType, ): ITotalWithData[] { const updatedTotals: ITotalWithData[] = cloneDeep(totals); remove(updatedTotals, total => total.type === totalItemTypeToRemove); return updatedTotals; } export function isTotalUsed(totals: ITotalWithData[], totalItemType: AFM.TotalType): boolean { return totals.some(row => row.type === totalItemType); } export function addTotalsRow(totals: ITotalWithData[], totalItemTypeToAdd: AFM.TotalType): ITotalWithData[] { const updatedTotals: ITotalWithData[] = cloneDeep(totals); if (isTotalUsed(updatedTotals, totalItemTypeToAdd)) { return updatedTotals; } const total: ITotalWithData = createTotalItem(totalItemTypeToAdd); updatedTotals.push(total); return updatedTotals; } export function updateTotalsRemovePosition( tableBoundingRect: ClientRect, totals: ITotalWithData[], isTotalsEditAllowed: boolean, totalsAreVisible: boolean, removeWrapper: HTMLElement, ): void { if (!isTotalsEditAllowed) { return; } const translateY: number = tableBoundingRect.height - getFooterHeight(totals, isTotalsEditAllowed, totalsAreVisible); removeWrapper.style.bottom = "auto"; removeWrapper.style.top = `${translateY}px`; } export function getAddTotalDropdownAlignPoints(isLastColumn: boolean = false): IAlignPoint[] { return isLastColumn ? [ { align: "tc br", offset: { x: 30, y: -3 } }, // top right { align: "bc tr", offset: { x: 30, y: 50 } }, // bottom right ] : [ { align: "tc bc", offset: { x: 0, y: -3 } }, // top center { align: "bc tc", offset: { x: 0, y: 50 } }, // bottom center ]; } export function shouldShowAddTotalButton( header: IMappingHeader, isFirstColumn: boolean, addingMoreTotalsEnabled: boolean, ): boolean { return !isFirstColumn && isMappingHeaderMeasureItem(header) && addingMoreTotalsEnabled; } export function getFirstMeasureIndex(headers: IMappingHeader[]): number { const measureOffset = headers.findIndex(header => isMappingHeaderMeasureItem(header)); return measureOffset === -1 ? 0 : measureOffset; } export function hasTableColumnTotalEnabled( outputMeasureIndexes: number[], tableColumnIndex: number, firstMeasureIndex: number, ): boolean { const index = tableColumnIndex - firstMeasureIndex; return outputMeasureIndexes && outputMeasureIndexes.includes(index); } export function addMeasureIndex( totals: ITotalWithData[], headers: IMappingHeader[], totalType: AFM.TotalType, tableColumnIndex: number, ): ITotalWithData[] { const index: number = tableColumnIndex - getFirstMeasureIndex(headers); return totals.map((total: ITotalWithData) => { if (total.type !== totalType) { return total; } const outputMeasureIndexes: number[] = clone(total.outputMeasureIndexes); outputMeasureIndexes.push(index); outputMeasureIndexes.sort((a, b) => a - b); return { ...total, outputMeasureIndexes: sortedUniq(outputMeasureIndexes), }; }); } export function removeMeasureIndex( totals: ITotalWithData[], headers: IMappingHeader[], totalType: AFM.TotalType, tableColumnIndex: number, ): ITotalWithData[] { const index: number = tableColumnIndex - getFirstMeasureIndex(headers); return totals.map((total: ITotalWithData) => { if (total.type !== totalType) { return total; } const outputMeasureIndexes: number[] = without(total.outputMeasureIndexes, index); return { ...total, outputMeasureIndexes, }; }); } export function getTotalsDefinition(totalsWithValues: ITotalWithData[]): IIndexedTotalItem[] { const totalsWithoutValues: IIndexedTotalItem[] = totalsWithValues.map( (total: IIndexedTotalItem) => omit(total, "values") as IIndexedTotalItem, ); return orderTotals(totalsWithoutValues); } export function shouldShowTotals(headers: IMappingHeader[]): boolean { if (headers.length < 1) { return false; } const onlyMeasures: boolean = headers.every((header: IMappingHeader) => isMappingHeaderMeasureItem(header), ); const onlyAttributes: boolean = headers.every((header: IMappingHeader) => isMappingHeaderAttribute(header), ); return !(onlyAttributes || onlyMeasures); } export const getColumnTotalsFromResultSpec = (source: AFM.IResultSpec) => { return get(source, "dimensions[0].totals", []); }; export default { getColumnTotalsFromResultSpec, };