diff --git a/frontend/src/metabase/lib/query/field.js b/frontend/src/metabase/lib/query/field.js index 266bfee8133a094c46561bd724000a983f87cc40..8fa7e367af1870553b1e689cadf05a94947a0a0d 100644 --- a/frontend/src/metabase/lib/query/field.js +++ b/frontend/src/metabase/lib/query/field.js @@ -3,6 +3,7 @@ import { mbqlEq } from "./util"; import type { Field as FieldReference } from "metabase/meta/types/Query"; import type { Field, FieldId, FieldValues } from "metabase/meta/types/Field"; +import type { Value } from "metabase/meta/types/Dataset"; // gets the target field ID (recursively) from any type of field, including raw field ID, fk->, and datetime-field cast. export function getFieldTargetId(field: FieldReference): ?FieldId { diff --git a/frontend/src/metabase/lib/schema_metadata.js b/frontend/src/metabase/lib/schema_metadata.js index ca632802a2eb9f951e29c53810211ad122567ef5..cdcc1e330d0c40739ea8762ea01acc8989993ea3 100644 --- a/frontend/src/metabase/lib/schema_metadata.js +++ b/frontend/src/metabase/lib/schema_metadata.js @@ -1,7 +1,7 @@ import _ from "underscore"; import { isa, isFK as isTypeFK, isPK as isTypePK, TYPE } from "metabase/lib/types"; -import { getFieldValues, getHumanReadableValue } from "metabase/lib/query/field"; +import { getFieldValues } from "metabase/lib/query/field"; // primary field types used for picking operators, etc export const NUMBER = "NUMBER"; @@ -182,7 +182,8 @@ function equivalentArgument(field, table) { .filter(([value, displayValue]) => value != null) .map(([value, displayValue]) => ({ key: value, - name: getHumanReadableValue(value, values) + // NOTE Atte Keinänen 8/7/17: Similar logic as in getHumanReadableValue of lib/query/field + name: displayValue ? displayValue : String(value) })) .sort((a, b) => a.key === b.key ? 0 : (a.key < b.key ? -1 : 1)) }; diff --git a/frontend/src/metabase/query_builder/containers/QueryBuilder.integ.spec.js b/frontend/src/metabase/query_builder/containers/QueryBuilder.integ.spec.js index fbdebfbdc2c34726dc930ff59c06da0c9fa96560..0482002aba7bf83ec0f757b70b8af918470afb9d 100644 --- a/frontend/src/metabase/query_builder/containers/QueryBuilder.integ.spec.js +++ b/frontend/src/metabase/query_builder/containers/QueryBuilder.integ.spec.js @@ -25,7 +25,12 @@ import { SET_ERROR_PAGE } from "metabase/redux/app"; import QueryHeader from "metabase/query_builder/components/QueryHeader"; import { VisualizationEmptyState } from "metabase/query_builder/components/QueryVisualization"; -import { FETCH_TABLE_METADATA } from "metabase/redux/metadata"; +import { + deleteFieldDimension, + updateFieldDimension, + updateFieldValues, + FETCH_TABLE_METADATA +} from "metabase/redux/metadata"; import FieldList, { DimensionPicker } from "metabase/query_builder/components/FieldList"; import FilterPopover from "metabase/query_builder/components/filters/FilterPopover"; import VisualizationError from "metabase/query_builder/components/VisualizationError"; @@ -51,6 +56,10 @@ import ChartClickActions from "metabase/visualizations/components/ChartClickActi import { delay } from "metabase/lib/promise"; +const REVIEW_PRODUCT_ID = 32; +const REVIEW_RATING_ID = 33; +const PRODUCT_TITLE_ID = 27; + const initQbWithDbAndTable = (dbId, tableId) => { return async () => { const store = await createTestStore() @@ -108,7 +117,7 @@ describe("QueryBuilder", () => { expect(table.find('div[children="Created At"]').length).toBe(1); - const doneButton = settingsModal.find(".Button--primary.disabled") + const doneButton = settingsModal.find(".Button--primary") expect(doneButton.length).toBe(1) const fieldsToIncludeCheckboxes = settingsModal.find(CheckBox) @@ -724,4 +733,124 @@ describe("QueryBuilder", () => { }); }) }) + + describe("remapping", () => { + beforeAll(async () => { + // add remappings + const store = await createTestStore() + + // NOTE Atte Keinänen 8/7/17: + // We test here the full dimension functionality which lets you enter a dimension name that differs + // from the field name. This is something that field settings UI doesn't let you to do yet. + + await store.dispatch(updateFieldDimension(REVIEW_PRODUCT_ID, { + type: "external", + name: "Product Name", + human_readable_field_id: PRODUCT_TITLE_ID + })); + + await store.dispatch(updateFieldDimension(REVIEW_RATING_ID, { + type: "internal", + name: "Rating Description", + human_readable_field_id: null + })); + await store.dispatch(updateFieldValues(REVIEW_RATING_ID, [ + [1, 'Awful'], [2, 'Unpleasant'], [3, 'Meh'], [4, 'Enjoyable'], [5, 'Perfecto'] + ])); + }) + + describe("for Rating category field with custom field values", () => { + // The following test case is very similar to earlier filter tests but in this case we use remapped values + it("lets you add 'Rating is Perfecto' filter", async () => { + const { store, qb } = await initQBWithReviewsTable(); + + // open filter popover + const filterSection = qb.find('.GuiBuilder-filtered-by'); + const newFilterButton = filterSection.find('.AddButton'); + newFilterButton.simulate("click"); + + // choose the field to be filtered + const filterPopover = filterSection.find(FilterPopover); + const ratingFieldButton = filterPopover.find(FieldList).find('h4[children="Rating Description"]') + expect(ratingFieldButton.length).toBe(1); + ratingFieldButton.simulate('click'); + + // check that field values seem correct + const fieldItems = filterPopover.find('li'); + expect(fieldItems.length).toBe(5); + expect(fieldItems.first().text()).toBe("Awful") + expect(fieldItems.last().text()).toBe("Perfecto") + + // select the last item (Perfecto) + const widgetFieldItem = fieldItems.last(); + const widgetCheckbox = widgetFieldItem.find(CheckBox); + expect(widgetCheckbox.props().checked).toBe(false); + widgetFieldItem.children().first().simulate("click"); + expect(widgetCheckbox.props().checked).toBe(true); + + // add the filter + const addFilterButton = filterPopover.find('button[children="Add filter"]') + addFilterButton.simulate("click"); + + await store.waitForActions([SET_DATASET_QUERY]) + store.resetDispatchedActions(); + + // validate the filter text value + expect(qb.find(FilterPopover).length).toBe(0); + const filterWidget = qb.find(FilterWidget); + expect(filterWidget.length).toBe(1); + expect(filterWidget.text()).toBe("Rating Description is equal toPerfecto"); + }) + + it("shows remapped value correctly in Raw Data query with Table visualization", async () => { + const { store, qb } = await initQBWithReviewsTable(); + + qb.find(RunButton).simulate("click"); + await store.waitForActions([QUERY_COMPLETED]); + + const table = qb.find(TestTable); + const headerCells = table.find("thead tr").first().find("th"); + const firstRowCells = table.find("tbody tr").first().find("td"); + + expect(headerCells.length).toBe(6) + expect(headerCells.at(4).text()).toBe("Rating Description") + + expect(firstRowCells.length).toBe(6); + + expect(firstRowCells.at(4).text()).toBe("Enjoyable"); + }) + }); + + describe("for Product ID FK field with a FK remapping", () => { + it("shows remapped values correctly in Raw Data query with Table visualization", async () => { + const { store, qb } = await initQBWithReviewsTable(); + + qb.find(RunButton).simulate("click"); + await store.waitForActions([QUERY_COMPLETED]); + + const table = qb.find(TestTable); + const headerCells = table.find("thead tr").first().find("th"); + const firstRowCells = table.find("tbody tr").first().find("td"); + + expect(headerCells.length).toBe(6) + expect(headerCells.at(3).text()).toBe("Product Name") + + expect(firstRowCells.length).toBe(6); + + expect(firstRowCells.at(3).text()).toBe("Ergonomic Leather Pants"); + }) + }); + + afterAll(async () => { + const store = await createTestStore() + + await store.dispatch(deleteFieldDimension(REVIEW_PRODUCT_ID)); + await store.dispatch(deleteFieldDimension(REVIEW_RATING_ID)); + + await store.dispatch(updateFieldValues(REVIEW_RATING_ID, [ + [1, '1'], [2, '2'], [3, '3'], [4, '4'], [5, '5'] + ])); + }) + + }) }); diff --git a/frontend/src/metabase/visualizations/components/TableSimple.jsx b/frontend/src/metabase/visualizations/components/TableSimple.jsx index c8ac3e8f94375ab35ae15aeca098b63ada92649d..2e68740a628bc06cb363e2ac9dc979c00a474f98 100644 --- a/frontend/src/metabase/visualizations/components/TableSimple.jsx +++ b/frontend/src/metabase/visualizations/components/TableSimple.jsx @@ -9,8 +9,7 @@ import ExplicitSize from "metabase/components/ExplicitSize.jsx"; import Ellipsified from "metabase/components/Ellipsified.jsx"; import Icon from "metabase/components/Icon.jsx"; -import { formatValue } from "metabase/lib/formatting"; -import { getFriendlyName } from "metabase/visualizations/lib/utils"; +import { formatColumn, formatValue } from "metabase/lib/formatting"; import { getTableCellClickedObject, isColumnRightAligned } from "metabase/visualizations/lib/table"; import cx from "classnames"; @@ -112,7 +111,7 @@ export default class TableSimple extends Component { width={8} height={8} style={{ position: "absolute", right: "100%", marginRight: 3 }} /> - <Ellipsified>{getFriendlyName(col)}</Ellipsified> + <Ellipsified>{formatColumn(col)}</Ellipsified> </div> </th> )} diff --git a/frontend/src/metabase/visualizations/lib/utils.js b/frontend/src/metabase/visualizations/lib/utils.js index 66316013ff08710efa172628184a631149b26079..a06b85ec50eb1a5b12e778034326760fa8156a18 100644 --- a/frontend/src/metabase/visualizations/lib/utils.js +++ b/frontend/src/metabase/visualizations/lib/utils.js @@ -135,7 +135,14 @@ export function getXValues(datas, chartType) { } export function getFriendlyName(column) { - return column.display_name || FRIENDLY_NAME_MAP[column.name.toLowerCase().trim()] || column.name; + if (column.display_name && column.display_name !== column.name) { + return column.display_name + } else { + // NOTE Atte Keinänen 8/7/17: + // Values `display_name` and `name` are same for breakout columns so check FRIENDLY_NAME_MAP + // before returning either `display_name` or `name` + return FRIENDLY_NAME_MAP[column.name.toLowerCase().trim()] || column.display_name || column.name; + } } export function getCardColors(card) {