Skip to content
Snippets Groups Projects
Commit 164344b6 authored by Tom Robinson's avatar Tom Robinson
Browse files

Merge branch 'dash-filter-search-widget' of github.com:metabase/metabase into...

Merge branch 'dash-filter-search-widget' of github.com:metabase/metabase into dash-filter-new-endpoints
parents 4f5fde14 e417feb8
Branches
Tags
No related merge requests found
......@@ -631,7 +631,7 @@ export default class StructuredQuery extends AtomicQuery {
const filteredNonFKDimensions = this.dimensions()
.filter(dimensionFilter)
.filter(d => !dimensionIsFKReference(d));
// .filter(d => !dimensionIsFKReference(d));
for (const dimension of filteredNonFKDimensions) {
fieldOptions.count++;
......
......@@ -42,6 +42,12 @@ export class FieldValuesWidget extends Component {
}
}
componentWillUnmount() {
if (this._cancel) {
this._cancel();
}
}
hasList() {
const { field } = this.props;
return field.has_field_values === "list" && field.values;
......@@ -104,6 +110,10 @@ export class FieldValuesWidget extends Component {
loadingState: "INIT",
});
if (this._cancel) {
this._cancel();
}
this._searchDebounced(value);
};
......@@ -173,12 +183,9 @@ export class FieldValuesWidget extends Component {
}
let options = [];
if (field.has_field_values === "list" && field.values) {
if (this.hasList()) {
options = field.values;
} else if (
field.has_field_values === "search" &&
loadingState === "LOADED"
) {
} else if (this.isSearchable() && loadingState === "LOADED") {
options = this.state.options;
} else {
options = [];
......@@ -220,7 +227,14 @@ export class FieldValuesWidget extends Component {
<div>
{valuesList}
{this.props.alwaysShowOptions || this.state.focused
? optionsList
? optionsList ||
(this.hasList() ? (
<OptionsMessage
message={t`Including every option in your filter probably won’t do much…`}
/>
) : this.isSearchable() && loadingState === "LOADED" ? (
<OptionsMessage message={t`No matching results found`} />
) : null)
: null}
</div>
)}
......@@ -262,14 +276,14 @@ export class FieldValuesWidget extends Component {
>
<LoadingSpinner size={32} />
</div>
) : loadingState === "LOADED" && options.length === 0 ? (
<div className="flex layout-centered p4">
{t`No matching results found`}
</div>
) : null}
</div>
);
}
}
const OptionsMessage = ({ message }) => (
<div className="flex layout-centered p4">{message}</div>
);
export default connect(null, mapDispatchToProps)(FieldValuesWidget);
......@@ -93,11 +93,11 @@ export default class TokenField extends Component {
};
componentWillMount() {
this._updateFilteredValues();
this._updateFilteredValues(this.props);
}
componentWillReceiveProps(nextProps, nextState) {
setTimeout(this._updateFilteredValues, 0);
componentWillReceiveProps(nextProps) {
this._updateFilteredValues(nextProps);
}
setInputValue(inputValue, setSearchValue = true) {
......@@ -107,22 +107,13 @@ export default class TokenField extends Component {
if (setSearchValue) {
newState.searchValue = inputValue;
}
this.setState(newState, this._updateFilteredValues);
this.setState(newState, () => this._updateFilteredValues(this.props));
}
clearInputValue(clearSearchValue = true) {
this.setInputValue("", clearSearchValue);
}
filterOption(option, searchValue) {
const { filterOption } = this.props;
if (filterOption) {
return filterOption(option, searchValue);
} else {
return String(this._label(option) || "").indexOf(searchValue) >= 0;
}
}
_value(option) {
const { valueKey } = this.props;
return typeof valueKey === "function" ? valueKey(option) : option[valueKey];
......@@ -143,11 +134,16 @@ export default class TokenField extends Component {
}
}
_updateFilteredValues = () => {
const { options, value, removeSelected } = this.props;
_updateFilteredValues = props => {
let { options, value, removeSelected, filterOption } = props;
let { searchValue, selectedOptionValue } = this.state;
let selectedValues = new Set(value.map(v => JSON.stringify(v)));
if (!filterOption) {
filterOption = (option, searchValue) =>
String(this._label(option) || "").indexOf(searchValue) >= 0;
}
let filteredOptions = options.filter(
option =>
// filter out options who have already been selected, unless:
......@@ -158,7 +154,7 @@ export default class TokenField extends Component {
// or it's the current "freeform" value, which updates as we type
(this._isLastFreeformValue(this._value(option)) &&
this._isLastFreeformValue(searchValue))) &&
this.filterOption(option, searchValue),
filterOption(option, searchValue),
);
if (
......@@ -268,9 +264,8 @@ export default class TokenField extends Component {
if (this.props.onFocus) {
this.props.onFocus();
}
this.setState(
{ focused: true, searchValue: this.state.inputValue },
this._updateFilteredValues,
this.setState({ focused: true, searchValue: this.state.inputValue }, () =>
this._updateFilteredValues(this.props),
);
};
......
......@@ -4,6 +4,7 @@ import { mount } from "enzyme";
import {
metadata,
PRODUCT_CATEGORY_FIELD_ID,
ORDERS_PRODUCT_FK_FIELD_ID
} from "../__support__/sample_dataset_fixture";
import { FieldValuesWidget } from "../../src/metabase/components/FieldValuesWidget";
......@@ -23,59 +24,114 @@ const mountFieldValuesWidget = props =>
);
describe("FieldValuesWidget", () => {
describe("has_field_values = none", () => {
const props = {
field: mock(metadata.field(PRODUCT_CATEGORY_FIELD_ID), {
has_field_values: "none",
}),
};
it("should not call fetchFieldValues", () => {
const fetchFieldValues = jest.fn();
mountFieldValuesWidget({ ...props, fetchFieldValues });
expect(fetchFieldValues).not.toHaveBeenCalled();
describe("category field", () => {
describe("has_field_values = none", () => {
const props = {
field: mock(metadata.field(PRODUCT_CATEGORY_FIELD_ID), {
has_field_values: "none",
}),
};
it("should not call fetchFieldValues", () => {
const fetchFieldValues = jest.fn();
mountFieldValuesWidget({ ...props, fetchFieldValues });
expect(fetchFieldValues).not.toHaveBeenCalled();
});
it("should have 'Enter some text' as the placeholder text", () => {
const component = mountFieldValuesWidget({ ...props });
expect(component.find(TokenField).props().placeholder).toEqual(
"Enter some text",
);
});
});
it("should have 'Enter some text' as the placeholder text", () => {
const component = mountFieldValuesWidget({ ...props });
expect(component.find(TokenField).props().placeholder).toEqual(
"Enter some text",
);
describe("has_field_values = list", () => {
const props = {
field: metadata.field(PRODUCT_CATEGORY_FIELD_ID),
};
it("should call fetchFieldValues", () => {
const fetchFieldValues = jest.fn();
mountFieldValuesWidget({ ...props, fetchFieldValues });
expect(fetchFieldValues).toHaveBeenCalledWith(
PRODUCT_CATEGORY_FIELD_ID,
);
});
it("should have 'Search the list' as the placeholder text", () => {
const component = mountFieldValuesWidget({ ...props });
expect(component.find(TokenField).props().placeholder).toEqual(
"Search the list",
);
});
});
});
describe("has_field_values = list", () => {
const props = {
field: metadata.field(PRODUCT_CATEGORY_FIELD_ID),
};
it("should call fetchFieldValues", () => {
const fetchFieldValues = jest.fn();
mountFieldValuesWidget({ ...props, fetchFieldValues });
expect(fetchFieldValues).toHaveBeenCalledWith(PRODUCT_CATEGORY_FIELD_ID);
});
it("should have 'Search the list' as the placeholder text", () => {
const component = mountFieldValuesWidget({ ...props });
expect(component.find(TokenField).props().placeholder).toEqual(
"Search the list",
);
describe("has_field_values = search", () => {
const props = {
field: mock(metadata.field(PRODUCT_CATEGORY_FIELD_ID), {
has_field_values: "search",
}),
searchField: metadata.field(PRODUCT_CATEGORY_FIELD_ID)
};
it("should not call fetchFieldValues", () => {
const fetchFieldValues = jest.fn();
mountFieldValuesWidget({ ...props, fetchFieldValues });
expect(fetchFieldValues).not.toHaveBeenCalled();
});
it("should have 'Search by Category' as the placeholder text", () => {
const component = mountFieldValuesWidget({ ...props });
expect(component.find(TokenField).props().placeholder).toEqual(
"Search by Category",
);
});
});
});
describe("has_field_values = search", () => {
const props = {
field: mock(metadata.field(PRODUCT_CATEGORY_FIELD_ID), {
has_field_values: "search",
}),
searchField: metadata
.field(PRODUCT_CATEGORY_FIELD_ID)
.filterSearchField(),
};
it("should not call fetchFieldValues", () => {
const fetchFieldValues = jest.fn();
mountFieldValuesWidget({ ...props, fetchFieldValues });
expect(fetchFieldValues).not.toHaveBeenCalled();
describe("id field", () => {
describe("has_field_values = none", () => {
it("should have 'Enter an ID' as the placeholder text", () => {
const component = mountFieldValuesWidget({
field: mock(metadata.field(ORDERS_PRODUCT_FK_FIELD_ID), {
has_field_values: "none",
}),
});
expect(component.find(TokenField).props().placeholder).toEqual(
"Enter an ID",
);
});
});
describe("has_field_values = list", () => {
it("should have 'Search the list' as the placeholder text", () => {
const component = mountFieldValuesWidget({
field: mock(metadata.field(ORDERS_PRODUCT_FK_FIELD_ID), {
has_field_values: "list",
values: [[1234]]
})
});
expect(component.find(TokenField).props().placeholder).toEqual(
"Search the list",
);
});
});
it("should have 'Search by Category' as the placeholder text", () => {
const component = mountFieldValuesWidget({ ...props });
expect(component.find(TokenField).props().placeholder).toEqual(
"Search by Category",
);
describe("has_field_values = search", () => {
it("should have 'Search by Category or enter an ID' as the placeholder text", () => {
const component = mountFieldValuesWidget({
field: mock(metadata.field(ORDERS_PRODUCT_FK_FIELD_ID), {
has_field_values: "search",
}),
searchField: metadata.field(PRODUCT_CATEGORY_FIELD_ID)
});
expect(component.find(TokenField).props().placeholder).toEqual(
"Search by Category or enter an ID",
);
});
it("should not duplicate 'ID' in placeholder when ID itself is searchable", () => {
const field = mock(metadata.field(ORDERS_PRODUCT_FK_FIELD_ID), {
base_type: "type/Text",
has_field_values: "search",
})
const component = mountFieldValuesWidget({
field: field,
searchField: field
});
expect(component.find(TokenField).props().placeholder).toEqual(
"Search by Product",
);
});
});
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment