UNPKG

@gooddata/react-components

Version:

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

648 lines (598 loc) • 21.4 kB
// (C) 2019-2020 GoodData Corporation import cloneDeep = require("lodash/cloneDeep"); import { AFM } from "@gooddata/typings"; import { createSorts, removeInvalidSort, getBucketItemIdentifiers, getSortIdentifiers, setSortItems, } from "../sort"; import { IExtendedReferencePoint, IVisualizationProperties } from "../../interfaces/Visualization"; import * as referencePointMocks from "../../mocks/referencePointMocks"; import { DEFAULT_BASE_CHART_UICONFIG } from "../../constants/uiConfig"; import { SORT_DIR_ASC, SORT_DIR_DESC } from "../../constants/sort"; import { FILTERS, METRIC, ATTRIBUTE } from "../../constants/bucket"; const emptyVisualizationProperties: IVisualizationProperties = {}; const emptyResultSpec: AFM.IResultSpec = {}; const emptyAfm: AFM.IAfm = {}; const A1 = { localIdentifier: "a1", displayForm: { identifier: "ident_a1", }, }; const A2 = { localIdentifier: "a2", displayForm: { identifier: "ident_a2", }, }; const A3 = { localIdentifier: "a3", displayForm: { identifier: "ident_a3", }, }; const M1 = { alias: "Measure m1", definition: { measure: { item: { identifier: "ident_m1", }, }, }, localIdentifier: "m1", }; const M2 = { alias: "Measure m2", definition: { measure: { item: { identifier: "ident_m2", }, }, }, localIdentifier: "m2", }; const SORTED_BY_A1_AGGREGATION: AFM.SortItem = { attributeSortItem: { aggregation: "sum", attributeIdentifier: "a1", direction: "desc", }, }; const SORTED_BY_A2_AGGREGATION: AFM.SortItem = { attributeSortItem: { aggregation: "sum", attributeIdentifier: "a2", direction: "desc", }, }; const SORTED_BY_M1: AFM.SortItem = { measureSortItem: { direction: "desc", locators: [ { measureLocatorItem: { measureIdentifier: "m1", }, }, ], }, }; const stackedAfm: AFM.IAfm = { measures: [M1], attributes: [A1, A2], }; const stackedResultSpec: AFM.IResultSpec = { dimensions: [ { itemIdentifiers: ["measureGroup", "a2"], }, { itemIdentifiers: ["a1"], }, ], }; const attributeSort: AFM.IAttributeSortItem = { attributeSortItem: { attributeIdentifier: "a1", direction: "asc", }, }; const measureSort: AFM.IMeasureSortItem = { measureSortItem: { direction: "asc", locators: [ { attributeLocatorItem: { attributeIdentifier: "a1", element: "/gdc/md/PROJECTID/obj/2210/elements?id=1234", }, }, { measureLocatorItem: { measureIdentifier: "m1", }, }, ], }, }; const referencePoint: IExtendedReferencePoint = { ...referencePointMocks.multipleMetricBucketsAndCategoryReferencePoint, uiConfig: DEFAULT_BASE_CHART_UICONFIG, properties: { sortItems: [attributeSort, measureSort], }, }; // make sure nothing modifies the root object so we can share it between tests Object.freeze(referencePoint); describe("createSorts", () => { describe("default sorting", () => { describe("table", () => { it("should sort by first attribute ASC", () => { const afm: AFM.IAfm = { attributes: [A1, A2], }; const expectedSorts: AFM.SortItem[] = [ { attributeSortItem: { attributeIdentifier: "a1", direction: "asc", }, }, ]; expect(createSorts("table", afm, emptyResultSpec, emptyVisualizationProperties)).toEqual( expectedSorts, ); }); it("should sort by first attribute ASC if there are some measures", () => { const expectedSorts: AFM.SortItem[] = [ { attributeSortItem: { attributeIdentifier: "a1", direction: "asc", }, }, ]; expect( createSorts("table", stackedAfm, emptyResultSpec, emptyVisualizationProperties), ).toEqual(expectedSorts); }); it("should sort by first measure DESC if there are no attributes", () => { const afm: AFM.IAfm = { measures: [M1], }; const expectedSort: AFM.SortItem[] = [ { measureSortItem: { direction: "desc", locators: [ { measureLocatorItem: { measureIdentifier: "m1", }, }, ], }, }, ]; expect(createSorts("table", afm, emptyResultSpec, emptyVisualizationProperties)).toEqual( expectedSort, ); }); }); describe("bar", () => { it("should sort by first measure for basic bar chart", () => { const afm: AFM.IAfm = { measures: [M1], attributes: [A1], }; const expectedSort: AFM.SortItem[] = [SORTED_BY_M1]; expect( createSorts("bar", afm, emptyResultSpec, emptyVisualizationProperties, false, true), ).toEqual(expectedSort); }); it("should sort by group for bar chart with 1 measure and 2 viewBy", () => { const afm: AFM.IAfm = { measures: [M1], attributes: [A1, A2], }; const resultSpec: AFM.IResultSpec = { dimensions: [ { itemIdentifiers: ["measureGroup"] }, { itemIdentifiers: ["a1", "a2"], }, ], }; const expectedSort: AFM.SortItem[] = [SORTED_BY_A1_AGGREGATION, SORTED_BY_A2_AGGREGATION]; expect( createSorts("bar", afm, resultSpec, emptyVisualizationProperties, false, true), ).toEqual(expectedSort); }); it("should sort by group for bar chart with 1 measure, 2 viewBy and 1 stackBy", () => { const afm: AFM.IAfm = { measures: [M1], attributes: [A1, A2, A3], }; const resultSpec: AFM.IResultSpec = { dimensions: [ { itemIdentifiers: ["a3"] }, { itemIdentifiers: ["a1", "a2", "measureGroup"], }, ], }; const expectedSort: AFM.SortItem[] = [SORTED_BY_A1_AGGREGATION, SORTED_BY_A2_AGGREGATION]; expect( createSorts("bar", afm, resultSpec, emptyVisualizationProperties, false, true), ).toEqual(expectedSort); }); it("should sort by group and first measure for bar chart with 2 measures and 2 viewBy", () => { const afm: AFM.IAfm = { measures: [M1, M2], attributes: [A1, A2], }; const resultSpec: AFM.IResultSpec = { dimensions: [ { itemIdentifiers: ["measureGroup"] }, { itemIdentifiers: ["a1", "a2"], }, ], }; const expectedSort: AFM.SortItem[] = [SORTED_BY_A1_AGGREGATION, SORTED_BY_M1]; expect( createSorts("bar", afm, resultSpec, emptyVisualizationProperties, false, true), ).toEqual(expectedSort); }); it("should sort by group for bar chart with 2 measures, 2 viewBy and canSortStackTotalValue is true", () => { const afm: AFM.IAfm = { measures: [M1, M2], attributes: [A1, A2, A3], }; const resultSpec: AFM.IResultSpec = { dimensions: [ { itemIdentifiers: ["a3"] }, { itemIdentifiers: ["a1", "a2", "measureGroup"], }, ], }; const expectedSort: AFM.SortItem[] = [SORTED_BY_A1_AGGREGATION, SORTED_BY_A2_AGGREGATION]; expect(createSorts("bar", afm, resultSpec, emptyVisualizationProperties, true, true)).toEqual( expectedSort, ); }); it("should not sort by total group", () => { const afm: AFM.IAfm = { measures: [M1], attributes: [A1, A2], }; const resultSpec: AFM.IResultSpec = { dimensions: [ { itemIdentifiers: ["measureGroup"] }, { itemIdentifiers: ["a1", "a2"], }, ], }; const expectedSort: AFM.SortItem[] = [SORTED_BY_M1]; expect( createSorts("bar", afm, resultSpec, emptyVisualizationProperties, false, false), ).toEqual(expectedSort); }); it("should return area sort for stacked bar chart", () => { const expectedSort: AFM.SortItem[] = [SORTED_BY_M1]; expect( createSorts( "bar", stackedAfm, emptyResultSpec, emptyVisualizationProperties, false, true, ), ).toEqual(expectedSort); }); }); describe("column", () => { it("should return empty array", () => { expect( createSorts("column", stackedAfm, stackedResultSpec, emptyVisualizationProperties), ).toEqual([]); }); }); describe("line", () => { it("should return empty array", () => { expect( createSorts("line", stackedAfm, stackedResultSpec, emptyVisualizationProperties), ).toEqual([]); }); }); describe("pie", () => { it("should return empty array", () => { expect( createSorts("pie", stackedAfm, stackedResultSpec, emptyVisualizationProperties), ).toEqual([]); }); }); describe("heatmap", () => { it("should return empty array when resultSpec's first dimension's itemIdentifier is measureGroup", () => { expect( createSorts("heatmap", stackedAfm, stackedResultSpec, emptyVisualizationProperties), ).toEqual([]); }); }); }); it("should extract sort from visualization properties", () => { const afm: AFM.IAfm = { measures: [M1], }; const visualizationProperties = { sortItems: [SORTED_BY_M1], }; expect(createSorts("table", afm, emptyResultSpec, visualizationProperties)).toEqual( visualizationProperties.sortItems, ); }); it("should ignore sort from visualization properties if localIdentifier is missing in AFM", () => { const visualizationProperties = { sortItems: [SORTED_BY_M1], }; expect(createSorts("table", emptyAfm, emptyResultSpec, visualizationProperties)).toEqual([]); }); it("should sort by segmentBy attribute if chart is geo Pushpin", () => { const afm: AFM.IAfm = { measures: [M1], attributes: [A1, A2], }; const resultSpec: AFM.IResultSpec = { dimensions: [ { itemIdentifiers: ["measureGroup"] }, { itemIdentifiers: ["a1", "a2"], }, ], }; expect(createSorts("pushpin", afm, resultSpec, emptyVisualizationProperties)).toEqual([ { attributeSortItem: { attributeIdentifier: "a2", direction: "asc", }, }, ]); }); }); describe("getBucketItemIdentifiers", () => { it("should get all identifiers", () => { expect(getBucketItemIdentifiers(referencePoint)).toEqual(["m1", "m2", "m3", "a1"]); }); }); describe("getSortIdentifiers", () => { it("should get all identifiers from sort items", () => { expect(getSortIdentifiers(attributeSort)).toEqual(["a1"]); expect(getSortIdentifiers(measureSort)).toEqual(["a1", "m1"]); }); }); describe("removeInvalidSort", () => { it("should remove sorts with with identifiers that are not included in buckets", () => { const invalidAttributeSort: AFM.IAttributeSortItem = { attributeSortItem: { attributeIdentifier: "invalid", direction: SORT_DIR_DESC, }, }; const invalidMeasureSort1: AFM.IMeasureSortItem = { measureSortItem: { direction: "asc", locators: [ { attributeLocatorItem: { attributeIdentifier: "invalid", element: "/gdc/md/PROJECTID/obj/2210/elements?id=1234", }, }, ], }, }; const invalidMeasureSort2: AFM.IMeasureSortItem = { measureSortItem: { direction: "asc", locators: [ { measureLocatorItem: { measureIdentifier: "invalid", }, }, ], }, }; const referencePointWithInvalidSortItems = { ...referencePoint, properties: { sortItems: [ ...referencePoint.properties.sortItems, invalidAttributeSort, invalidMeasureSort1, invalidMeasureSort2, ], }, }; const sanitizedSortItems = removeInvalidSort(referencePointWithInvalidSortItems).properties.sortItems; expect(sanitizedSortItems).toEqual(referencePoint.properties.sortItems); }); }); describe("setSortItems", () => { const emptyReferencePoint: IExtendedReferencePoint = { buckets: [], filters: { localIdentifier: FILTERS, items: [], }, uiConfig: { buckets: {}, }, properties: { sortItems: [], }, }; it("should return unchanged reference if there are sort properties present", () => { const measureSortItem: AFM.IMeasureSortItem = { measureSortItem: { direction: "asc", locators: [ { measureLocatorItem: { measureIdentifier: "invalid", }, }, ], }, }; const referencePointWithSortItems: IExtendedReferencePoint = cloneDeep({ ...emptyReferencePoint, properties: { sortItems: [measureSortItem], }, }); expect(setSortItems(referencePointWithSortItems)).toEqual(referencePointWithSortItems); }); it("should return unchanged reference if there are no sort properties nor bucket items present", () => { const noSortNoItemsRefPoint: IExtendedReferencePoint = cloneDeep({ ...emptyReferencePoint, }); expect(setSortItems(noSortNoItemsRefPoint)).toEqual(noSortNoItemsRefPoint); }); it("should add descending sort item based on first measure if there is a measure but no attribute", () => { const onlyMeasureRefPoint: IExtendedReferencePoint = cloneDeep({ ...emptyReferencePoint, buckets: [ { localIdentifier: "measures", items: [ { type: METRIC, localIdentifier: "m1", }, { type: METRIC, localIdentifier: "m2", }, ], }, ], }); expect(setSortItems(onlyMeasureRefPoint).properties).toEqual({ sortItems: [ { measureSortItem: { direction: SORT_DIR_DESC, locators: [ { measureLocatorItem: { measureIdentifier: "m1", }, }, ], }, }, ], }); }); it("should skip invalid arithmetic measures", () => { const onlyMeasureRefPoint: IExtendedReferencePoint = cloneDeep({ ...emptyReferencePoint, buckets: [ { localIdentifier: "measures", items: [ { type: METRIC, localIdentifier: "am1", operator: "sum", operandLocalIdentifiers: ["m1", null], }, { type: METRIC, localIdentifier: "am2", operator: "sum", operandLocalIdentifiers: ["m1", "m1"], }, { type: METRIC, localIdentifier: "m1", }, ], }, ], }); expect(setSortItems(onlyMeasureRefPoint).properties).toEqual({ sortItems: [ { measureSortItem: { direction: SORT_DIR_DESC, locators: [ { measureLocatorItem: { measureIdentifier: "am2", }, }, ], }, }, ], }); }); it("should add ascending sort item based on first attribute if there is an attribute but no measure", () => { const onlyAttributeRefPoint: IExtendedReferencePoint = cloneDeep({ ...emptyReferencePoint, buckets: [ { localIdentifier: "measures", items: [ { type: ATTRIBUTE, localIdentifier: "a1", }, ], }, ], }); expect(setSortItems(onlyAttributeRefPoint).properties).toEqual({ sortItems: [ { attributeSortItem: { attributeIdentifier: "a1", direction: SORT_DIR_ASC, }, }, ], }); }); it("should not add any sort items if there are both measure and attribute", () => { const refPoint: IExtendedReferencePoint = cloneDeep({ ...emptyReferencePoint, buckets: [ { localIdentifier: "misc", items: [ { type: ATTRIBUTE, localIdentifier: "a1", }, { type: METRIC, localIdentifier: "m1", }, ], }, ], }); expect(setSortItems(refPoint)).toBe(refPoint); }); });