@gooddata/react-components
Version:
GoodData.UI - A powerful JavaScript library for building analytical applications
648 lines (598 loc) • 21.4 kB
text/typescript
// (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);
});
});