Skip to content
Snippets Groups Projects
Unverified Commit c9fc20b3 authored by Dalton's avatar Dalton Committed by GitHub
Browse files

move parameter operator logic into own utils file (#18633)

* move parameter type utils into own file

* move parameter operator logic into own utils file

* update reference in meta/Dashboard.js

* move parameter filter utils to own file (#18634)
parent 09b0c53a
Branches
Tags
No related merge requests found
......@@ -12,7 +12,7 @@ import { isa, TYPE } from "metabase/lib/types";
import {
dimensionFilterForParameter,
variableFilterForParameter,
} from "metabase/meta/Parameter";
} from "metabase/parameters/utils/filters";
export function getDataFromClicked({
extraData: { dashboard, parameterValuesBySlug, userAttributes } = {},
......
......@@ -16,15 +16,16 @@ import type {
} from "metabase-types/types/Parameter";
import {
dimensionFilterForParameter,
getTagOperatorFilterForParameter,
variableFilterForParameter,
getParameterOptions,
PARAMETER_OPERATOR_TYPES,
getOperatorDisplayName,
getParameterTargetField,
} from "metabase/meta/Parameter";
import { getOperatorDisplayName } from "metabase/parameters/utils/operators";
import {
dimensionFilterForParameter,
getTagOperatorFilterForParameter,
variableFilterForParameter,
} from "metabase/parameters/utils/filters";
import { slugify } from "metabase/lib/formatting";
export type ParameterSection = {
......
......@@ -23,24 +23,15 @@ import Dimension, {
import moment from "moment";
import { t } from "ttag";
import _ from "underscore";
import {
doesOperatorExist,
getOperatorByTypeAndName,
STRING,
NUMBER,
PRIMARY_KEY,
} from "metabase/lib/schema_metadata";
import {
getParameterType,
getParameterSubType,
} from "metabase/parameters/utils/parameter-type";
import Variable, { TemplateTagVariable } from "metabase-lib/lib/Variable";
type DimensionFilter = (dimension: Dimension) => boolean;
type TemplateTagFilter = (tag: TemplateTag) => boolean;
type FieldPredicate = (field: Field) => boolean;
type VariableFilter = (variable: Variable) => boolean;
import {
getOperatorDisplayName,
getParameterOperatorName,
} from "metabase/parameters/utils/operators";
import { fieldFilterForParameter } from "metabase/parameters/utils/filters";
const areFieldFilterOperatorsEnabled = () =>
MetabaseSettings.get("field-filter-operators-enabled?");
......@@ -207,54 +198,6 @@ function buildOperatorSubtypeOptions({ type, typeName }) {
}));
}
export function getOperatorDisplayName(option, operatorType, sectionName) {
if (operatorType === "date" || operatorType === "number") {
return option.name;
} else if (operatorType === "string" && option.operator === "=") {
return sectionName;
} else {
return `${sectionName} ${option.name.toLowerCase()}`;
}
}
function fieldFilterForParameter(parameter: Parameter): FieldPredicate {
const type = getParameterType(parameter);
const subtype = getParameterSubType(parameter);
switch (type) {
case "date":
return (field: Field) => field.isDate();
case "id":
return (field: Field) => field.isID();
case "category":
return (field: Field) => field.isCategory();
case "location":
return (field: Field) => {
switch (subtype) {
case "city":
return field.isCity();
case "state":
return field.isState();
case "zip_code":
return field.isZipCode();
case "country":
return field.isCountry();
default:
return field.isLocation();
}
};
case "number":
return (field: Field) => field.isNumber() && !field.isCoordinate();
case "string":
return (field: Field) => {
return subtype === "=" || subtype === "!="
? field.isCategory() && !field.isLocation()
: field.isString() && !field.isLocation();
};
}
return (field: Field) => false;
}
export function parameterOptionsForField(field: Field): ParameterOption[] {
return getParameterOptions()
.filter(option => fieldFilterForParameter(option)(field))
......@@ -266,64 +209,6 @@ export function parameterOptionsForField(field: Field): ParameterOption[] {
});
}
export function dimensionFilterForParameter(
parameter: Parameter,
): DimensionFilter {
const fieldFilter = fieldFilterForParameter(parameter);
return dimension => fieldFilter(dimension.field());
}
export function getTagOperatorFilterForParameter(parameter) {
const subtype = getParameterSubType(parameter);
const parameterOperatorName = getParameterOperatorName(subtype);
return tag => {
const { "widget-type": widgetType } = tag;
const subtype = getParameterSubType(widgetType);
const tagOperatorName = getParameterOperatorName(subtype);
return parameterOperatorName === tagOperatorName;
};
}
export function variableFilterForParameter(
parameter: Parameter,
): VariableFilter {
const tagFilter = tagFilterForParameter(parameter);
return variable => {
if (variable instanceof TemplateTagVariable) {
const tag = variable.tag();
return tag ? tagFilter(tag) : false;
}
return false;
};
}
function tagFilterForParameter(parameter: Parameter): TemplateTagFilter {
const type = getParameterType(parameter);
const subtype = getParameterSubType(parameter);
const operator = getParameterOperatorName(subtype);
if (operator !== "=") {
return (tag: TemplateTag) => false;
}
switch (type) {
case "date":
return (tag: TemplateTag) => subtype === "single" && tag.type === "date";
case "location":
return (tag: TemplateTag) => tag.type === "number" || tag.type === "text";
case "id":
return (tag: TemplateTag) => tag.type === "number" || tag.type === "text";
case "category":
return (tag: TemplateTag) => tag.type === "number" || tag.type === "text";
case "number":
return (tag: TemplateTag) => tag.type === "number";
case "string":
return (tag: TemplateTag) => tag.type === "text";
}
return (tag: TemplateTag) => false;
}
// NOTE: this should mirror `template-tag-parameters` in src/metabase/api/embed.clj
export function getTemplateTagParameters(tags: TemplateTag[]): Parameter[] {
function getTemplateTagType(tag) {
......@@ -559,35 +444,6 @@ export function normalizeParameterValue(type, value) {
}
}
function getParameterOperatorName(maybeOperatorName) {
return doesOperatorExist(maybeOperatorName) ? maybeOperatorName : "=";
}
export function deriveFieldOperatorFromParameter(parameter) {
const type = getParameterType(parameter);
const subtype = getParameterSubType(parameter);
const operatorType = getParameterOperatorType(type);
const operatorName = getParameterOperatorName(subtype);
return getOperatorByTypeAndName(operatorType, operatorName);
}
function getParameterOperatorType(parameterType) {
switch (parameterType) {
case "number":
return NUMBER;
case "string":
case "category":
case "location":
return STRING;
case "id":
// id can technically be a FK but doesn't matter as both use default filter operators
return PRIMARY_KEY;
default:
return undefined;
}
}
export function getValuePopulatedParameters(parameters, parameterValues) {
return parameterValues
? parameters.map(parameter => {
......
......@@ -23,10 +23,8 @@ import {
makeGetMergedParameterFieldValues,
} from "metabase/selectors/metadata";
import {
getParameterIconName,
deriveFieldOperatorFromParameter,
} from "metabase/meta/Parameter";
import { getParameterIconName } from "metabase/meta/Parameter";
import { deriveFieldOperatorFromParameter } from "metabase/parameters/utils/operators";
import { isDashboardParameterWithoutMapping } from "metabase/meta/Dashboard";
import S from "./ParameterWidget.css";
......
import { getParameterType, getParameterSubType } from "./parameter-type";
import { getParameterOperatorName } from "./operators";
import { TemplateTagVariable } from "metabase-lib/lib/Variable";
export function fieldFilterForParameter(parameter) {
const type = getParameterType(parameter);
const subtype = getParameterSubType(parameter);
switch (type) {
case "date":
return field => field.isDate();
case "id":
return field => field.isID();
case "category":
return field => field.isCategory();
case "location":
return field => {
switch (subtype) {
case "city":
return field.isCity();
case "state":
return field.isState();
case "zip_code":
return field.isZipCode();
case "country":
return field.isCountry();
default:
return field.isLocation();
}
};
case "number":
return field => field.isNumber() && !field.isCoordinate();
case "string":
return field => {
return subtype === "=" || subtype === "!="
? field.isCategory() && !field.isLocation()
: field.isString() && !field.isLocation();
};
}
return () => false;
}
export function dimensionFilterForParameter(parameter) {
const fieldFilter = fieldFilterForParameter(parameter);
return dimension => fieldFilter(dimension.field());
}
export function getTagOperatorFilterForParameter(parameter) {
const subtype = getParameterSubType(parameter);
const parameterOperatorName = getParameterOperatorName(subtype);
return tag => {
const { "widget-type": widgetType } = tag;
const subtype = getParameterSubType(widgetType);
const tagOperatorName = getParameterOperatorName(subtype);
return parameterOperatorName === tagOperatorName;
};
}
export function variableFilterForParameter(parameter) {
const tagFilter = tagFilterForParameter(parameter);
return variable => {
if (variable instanceof TemplateTagVariable) {
const tag = variable.tag();
return tag ? tagFilter(tag) : false;
}
return false;
};
}
function tagFilterForParameter(parameter) {
const type = getParameterType(parameter);
const subtype = getParameterSubType(parameter);
const operator = getParameterOperatorName(subtype);
if (operator !== "=") {
return () => false;
}
switch (type) {
case "date":
return tag => subtype === "single" && tag.type === "date";
case "location":
return tag => tag.type === "number" || tag.type === "text";
case "id":
return tag => tag.type === "number" || tag.type === "text";
case "category":
return tag => tag.type === "number" || tag.type === "text";
case "number":
return tag => tag.type === "number";
case "string":
return tag => tag.type === "text";
}
return () => false;
}
import {
dimensionFilterForParameter,
getTagOperatorFilterForParameter,
} from "./filters";
describe("parameters/utils/field-filters", () => {
describe("dimensionFilterForParameter", () => {
const field = {
isDate: () => false,
isID: () => false,
isCategory: () => false,
isCity: () => false,
isState: () => false,
isZipCode: () => false,
isCountry: () => false,
isNumber: () => false,
isString: () => false,
isLocation: () => false,
};
const typelessDimension = {
field: () => field,
};
[
[
{ type: "date/single" },
{
type: "date",
field: () => ({ ...field, isDate: () => true }),
},
],
[
{ type: "id" },
{
type: "id",
field: () => ({ ...field, isID: () => true }),
},
],
[
{ type: "category" },
{
type: "category",
field: () => ({ ...field, isCategory: () => true }),
},
],
[
{ type: "location/city" },
{
type: "city",
field: () => ({ ...field, isCity: () => true }),
},
],
[
{ type: "number/!=" },
{
type: "number",
field: () => ({
...field,
isNumber: () => true,
isCoordinate: () => false,
}),
},
],
[
{ type: "string/=" },
{
type: "category",
field: () => ({
...field,
isCategory: () => true,
}),
},
],
[
{ type: "string/!=" },
{
type: "category",
field: () => ({
...field,
isCategory: () => true,
}),
},
],
[
{ type: "string/starts-with" },
{
type: "string",
field: () => ({
...field,
isString: () => true,
}),
},
],
].forEach(([parameter, dimension]) => {
it(`should return a predicate that evaluates to true for a ${dimension.type} dimension when given a ${parameter.type} parameter`, () => {
const predicate = dimensionFilterForParameter(parameter);
expect(predicate(typelessDimension)).toBe(false);
expect(predicate(dimension)).toBe(true);
});
});
it("should return a predicate that evaluates to false for a coordinate dimension when given a number parameter", () => {
const coordinateDimension = {
field: () => ({
...field,
isNumber: () => true,
isCoordinate: () => true,
}),
};
const predicate = dimensionFilterForParameter({ type: "number/between" });
expect(predicate(coordinateDimension)).toBe(false);
});
it("should return a predicate that evaluates to false for a location dimension when given a category parameter", () => {
const locationDimension = {
field: () => ({
...field,
isLocation: () => true,
}),
};
const predicate = dimensionFilterForParameter({ type: "category" });
expect(predicate(locationDimension)).toBe(false);
});
});
describe("getTagOperatorFilterForParameter", () => {
it("should return a predicate that evaluates to true for a template tag that has the same subtype operator as the given parameter", () => {
const predicate = getTagOperatorFilterForParameter({
type: "string/starts-with",
});
const templateTag1 = {
"widget-type": "string/starts-with",
};
const templateTag2 = {
"widget-type": "foo/starts-with",
};
const templateTag3 = {
"widget-type": "string/ends-with",
};
expect(predicate(templateTag1)).toBe(true);
expect(predicate(templateTag2)).toBe(true);
expect(predicate(templateTag3)).toBe(false);
});
});
});
import { getParameterType, getParameterSubType } from "./parameter-type";
import {
doesOperatorExist,
getOperatorByTypeAndName,
NUMBER,
STRING,
PRIMARY_KEY,
} from "metabase/lib/schema_metadata";
export function getOperatorDisplayName(option, operatorType, sectionName) {
if (operatorType === "date" || operatorType === "number") {
return option.name;
} else if (operatorType === "string" && option.operator === "=") {
return sectionName;
} else {
return `${sectionName} ${option.name.toLowerCase()}`;
}
}
export function getParameterOperatorName(maybeOperatorName) {
return doesOperatorExist(maybeOperatorName) ? maybeOperatorName : "=";
}
export function deriveFieldOperatorFromParameter(parameter) {
const type = getParameterType(parameter);
const subtype = getParameterSubType(parameter);
const operatorType = getParameterOperatorType(type);
const operatorName = getParameterOperatorName(subtype);
return getOperatorByTypeAndName(operatorType, operatorName);
}
function getParameterOperatorType(parameterType) {
switch (parameterType) {
case "number":
return NUMBER;
case "string":
case "category":
case "location":
return STRING;
case "id":
// id can technically be a FK but doesn't matter as both use default filter operators
return PRIMARY_KEY;
default:
return undefined;
}
}
import {
getOperatorDisplayName,
deriveFieldOperatorFromParameter,
} from "./operators";
describe("parameters/utils/operators", () => {
describe("deriveFieldOperatorFromParameter", () => {
describe("getOperatorDisplayName", () => {
it("should return an option's name when the operator is a date or a number", () => {
expect(getOperatorDisplayName({ name: "foo" }, "date")).toEqual("foo");
expect(getOperatorDisplayName({ name: "foo" }, "number")).toEqual(
"foo",
);
});
it("should return an option's section name for the string/= option", () => {
expect(
getOperatorDisplayName(
{ name: "foo", operator: "=" },
"string",
"bar",
),
).toEqual("bar");
});
it("should otherwise return a combined sectionName + option name", () => {
expect(
getOperatorDisplayName(
{ name: "Foo", operator: "!=" },
"string",
"Bar",
),
).toEqual("Bar foo");
});
});
describe("when parameter is associated with an operator", () => {
it("should return relevant operator object", () => {
const operator2 = deriveFieldOperatorFromParameter({
type: "string/contains",
});
const operator3 = deriveFieldOperatorFromParameter({
type: "number/between",
});
expect(operator2.name).toEqual("contains");
expect(operator3.name).toEqual("between");
});
});
describe("when parameter is location/category", () => {
it("should map to an = operator", () => {
expect(
deriveFieldOperatorFromParameter({
type: "location/city",
}).name,
).toBe("=");
expect(
deriveFieldOperatorFromParameter({
type: "category",
}).name,
).toBe("=");
});
});
describe("when parameter is NOT associated with an operator", () => {
it("should return undefined", () => {
expect(deriveFieldOperatorFromParameter({ type: "date/single" })).toBe(
undefined,
);
});
});
});
});
......@@ -4,9 +4,6 @@ import MetabaseSettings from "metabase/lib/settings";
import {
PARAMETER_OPERATOR_TYPES,
getParameterOptions,
getOperatorDisplayName,
dimensionFilterForParameter,
getTagOperatorFilterForParameter,
getParameterTargetField,
dateParameterValueToMBQL,
stringParameterValueToMBQL,
......@@ -14,7 +11,6 @@ import {
parameterToMBQLFilter,
parameterOptionsForField,
normalizeParameterValue,
deriveFieldOperatorFromParameter,
getTemplateTagParameters,
getValuePopulatedParameters,
getParameterValueFromQueryParams,
......@@ -103,29 +99,6 @@ describe("metabase/meta/Parameter", () => {
});
});
describe("getOperatorDisplayName", () => {
it("should return an option's name when the operator is a date or a number", () => {
expect(getOperatorDisplayName({ name: "foo" }, "date")).toEqual("foo");
expect(getOperatorDisplayName({ name: "foo" }, "number")).toEqual("foo");
});
it("should return an option's section name for the string/= option", () => {
expect(
getOperatorDisplayName({ name: "foo", operator: "=" }, "string", "bar"),
).toEqual("bar");
});
it("should otherwise return a combined sectionName + option name", () => {
expect(
getOperatorDisplayName(
{ name: "Foo", operator: "!=" },
"string",
"Bar",
),
).toEqual("Bar foo");
});
});
describe("dateParameterValueToMBQL", () => {
it("should parse past30days", () => {
expect(dateParameterValueToMBQL("past30days", null)).toEqual([
......@@ -447,147 +420,6 @@ describe("metabase/meta/Parameter", () => {
});
});
describe("dimensionFilterForParameter", () => {
const field = {
isDate: () => false,
isID: () => false,
isCategory: () => false,
isCity: () => false,
isState: () => false,
isZipCode: () => false,
isCountry: () => false,
isNumber: () => false,
isString: () => false,
isLocation: () => false,
};
const typelessDimension = {
field: () => field,
};
[
[
{ type: "date/single" },
{
type: "date",
field: () => ({ ...field, isDate: () => true }),
},
],
[
{ type: "id" },
{
type: "id",
field: () => ({ ...field, isID: () => true }),
},
],
[
{ type: "category" },
{
type: "category",
field: () => ({ ...field, isCategory: () => true }),
},
],
[
{ type: "location/city" },
{
type: "city",
field: () => ({ ...field, isCity: () => true }),
},
],
[
{ type: "number/!=" },
{
type: "number",
field: () => ({
...field,
isNumber: () => true,
isCoordinate: () => false,
}),
},
],
[
{ type: "string/=" },
{
type: "category",
field: () => ({
...field,
isCategory: () => true,
}),
},
],
[
{ type: "string/!=" },
{
type: "category",
field: () => ({
...field,
isCategory: () => true,
}),
},
],
[
{ type: "string/starts-with" },
{
type: "string",
field: () => ({
...field,
isString: () => true,
}),
},
],
].forEach(([parameter, dimension]) => {
it(`should return a predicate that evaluates to true for a ${dimension.type} dimension when given a ${parameter.type} parameter`, () => {
const predicate = dimensionFilterForParameter(parameter);
expect(predicate(typelessDimension)).toBe(false);
expect(predicate(dimension)).toBe(true);
});
});
it("should return a predicate that evaluates to false for a coordinate dimension when given a number parameter", () => {
const coordinateDimension = {
field: () => ({
...field,
isNumber: () => true,
isCoordinate: () => true,
}),
};
const predicate = dimensionFilterForParameter({ type: "number/between" });
expect(predicate(coordinateDimension)).toBe(false);
});
it("should return a predicate that evaluates to false for a location dimension when given a category parameter", () => {
const locationDimension = {
field: () => ({
...field,
isLocation: () => true,
}),
};
const predicate = dimensionFilterForParameter({ type: "category" });
expect(predicate(locationDimension)).toBe(false);
});
});
describe("getTagOperatorFilterForParameter", () => {
it("should return a predicate that evaluates to true for a template tag that has the same subtype operator as the given parameter", () => {
const predicate = getTagOperatorFilterForParameter({
type: "string/starts-with",
});
const templateTag1 = {
"widget-type": "string/starts-with",
};
const templateTag2 = {
"widget-type": "foo/starts-with",
};
const templateTag3 = {
"widget-type": "string/ends-with",
};
expect(predicate(templateTag1)).toBe(true);
expect(predicate(templateTag2)).toBe(true);
expect(predicate(templateTag3)).toBe(false);
});
});
describe("getParameterTargetField", () => {
it("should return null when the target is not a dimension", () => {
expect(getParameterTargetField(["variable", "foo"], metadata)).toBe(null);
......@@ -644,45 +476,6 @@ describe("metabase/meta/Parameter", () => {
});
});
describe("deriveFieldOperatorFromParameter", () => {
describe("when parameter is associated with an operator", () => {
it("should return relevant operator object", () => {
const operator2 = deriveFieldOperatorFromParameter({
type: "string/contains",
});
const operator3 = deriveFieldOperatorFromParameter({
type: "number/between",
});
expect(operator2.name).toEqual("contains");
expect(operator3.name).toEqual("between");
});
});
describe("when parameter is location/category", () => {
it("should map to an = operator", () => {
expect(
deriveFieldOperatorFromParameter({
type: "location/city",
}).name,
).toBe("=");
expect(
deriveFieldOperatorFromParameter({
type: "category",
}).name,
).toBe("=");
});
});
describe("when parameter is NOT associated with an operator", () => {
it("should return undefined", () => {
expect(deriveFieldOperatorFromParameter({ type: "date/single" })).toBe(
undefined,
);
});
});
});
describe("getTemplateTagParameters", () => {
let tags;
beforeEach(() => {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment