diff --git a/frontend/src/metabase-lib/lib/queries/NativeQuery.js b/frontend/src/metabase-lib/lib/queries/NativeQuery.js index 3b0e3b03e232082c294fa70c2a184170ef884760..ae91c0c85140229749c158fd2b980019e2c3982c 100644 --- a/frontend/src/metabase-lib/lib/queries/NativeQuery.js +++ b/frontend/src/metabase-lib/lib/queries/NativeQuery.js @@ -63,7 +63,11 @@ export default class NativeQuery extends AtomicQuery { } canRun() { - return this.hasData() && this.queryText().length > 0; + return ( + this.hasData() && + this.queryText().length > 0 && + this.allTemplateTagsAreValid() + ); } isEmpty() { @@ -226,6 +230,12 @@ export default class NativeQuery extends AtomicQuery { templateTagsMap(): TemplateTags { return getIn(this.datasetQuery(), ["native", "template-tags"]) || {}; } + allTemplateTagsAreValid(): boolean { + return this.templateTags().every( + // field filters require a field + t => !(t.type === "dimension" && t.dimension == null), + ); + } setDatasetQuery(datasetQuery: DatasetQuery): NativeQuery { return new NativeQuery(this._originalQuestion, datasetQuery); @@ -282,7 +292,7 @@ export default class NativeQuery extends AtomicQuery { id: Utils.uuid(), name: tagName, display_name: humanize(tagName), - type: null, + type: "text", }; } } diff --git a/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx b/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx index 23c4e291ac39fe88a1dcc7bfebae64043ddb86df..7e291f3b37c56e3ae54070c97af9dc810114b380 100644 --- a/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx +++ b/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx @@ -160,7 +160,12 @@ export default class TagEditorParam extends Component { {tag.type === "dimension" && ( <div className="pb1"> - <h5 className="pb1 text-normal">{t`Field to map to`}</h5> + <h5 className="pb1 text-normal"> + {t`Field to map to`} + {tag.dimension == null && ( + <span className="text-error mx1">(required)</span> + )} + </h5> {(!hasSelectedDimensionField || (hasSelectedDimensionField && fieldMetadataLoaded)) && ( @@ -179,41 +184,43 @@ export default class TagEditorParam extends Component { </div> )} - <div className="pb1"> - <h5 className="pb1 text-normal">{t`Filter widget type`}</h5> - <Select - className="border-med bg-white block" - value={tag["widget-type"]} - onChange={e => - this.setParameterAttribute("widget-type", e.target.value) - } - isInitiallyOpen={!tag["widget-type"] && hasWidgetOptions} - placeholder={t`Select…`} - > - {[{ name: "None", type: undefined }] - .concat(widgetOptions) - .map(widgetOption => ( - <Option key={widgetOption.type} value={widgetOption.type}> - {widgetOption.name} - </Option> - ))} - </Select> - {hasSelectedDimensionField && !hasWidgetOptions && ( - <p className="pb1"> - {t`There aren't any filter widgets for this type of field yet.`}{" "} - <Link - to={MetabaseSettings.docsUrl( - "users-guide/13-sql-parameters", - "the-field-filter-variable-type", - )} - target="_blank" - className="link" - > - {t`Learn more`} - </Link> - </p> - )} - </div> + {hasSelectedDimensionField && ( + <div className="pb1"> + <h5 className="pb1 text-normal">{t`Filter widget type`}</h5> + <Select + className="border-med bg-white block" + value={tag["widget-type"]} + onChange={e => + this.setParameterAttribute("widget-type", e.target.value) + } + isInitiallyOpen={!tag["widget-type"] && hasWidgetOptions} + placeholder={t`Select…`} + > + {[{ name: "None", type: undefined }] + .concat(widgetOptions) + .map(widgetOption => ( + <Option key={widgetOption.type} value={widgetOption.type}> + {widgetOption.name} + </Option> + ))} + </Select> + {!hasWidgetOptions && ( + <p className="pb1"> + {t`There aren't any filter widgets for this type of field yet.`}{" "} + <Link + to={MetabaseSettings.docsUrl( + "users-guide/13-sql-parameters", + "the-field-filter-variable-type", + )} + target="_blank" + className="link" + > + {t`Learn more`} + </Link> + </p> + )} + </div> + )} <div className="flex align-center pb1"> <h5 className="text-normal mr1">{t`Required?`}</h5> diff --git a/frontend/test/metabase-lib/lib/queries/NativeQuery.unit.spec.js b/frontend/test/metabase-lib/lib/queries/NativeQuery.unit.spec.js index 9903ee97c0db230742fc30d4632414b627630157..ca3752e3d5a1229e73afda3023c62ae36443d4ad 100644 --- a/frontend/test/metabase-lib/lib/queries/NativeQuery.unit.spec.js +++ b/frontend/test/metabase-lib/lib/queries/NativeQuery.unit.spec.js @@ -1,3 +1,5 @@ +import { assocIn } from "icepick"; + import { SAMPLE_DATASET, MONGO_DATABASE, @@ -170,5 +172,29 @@ describe("NativeQuery", () => { expect(tagMaps["max_price"].display_name).toEqual("Max price"); }); }); + describe("Invalid template tags prevent the query from running", () => { + let q = makeQuery().setQueryText("SELECT * from ORDERS where {{foo}}"); + expect(q.canRun()).toBe(true); + + // set template tag's type to dimension without setting field id + q = q.setDatasetQuery( + assocIn( + q.datasetQuery(), + ["native", "template-tags", "foo", "type"], + "dimension", + ), + ); + expect(q.canRun()).toBe(false); + + // now set the field + q = q.setDatasetQuery( + assocIn( + q.datasetQuery(), + ["native", "template-tags", "foo", "dimension"], + ["field-id", 123], + ), + ); + expect(q.canRun()).toBe(true); + }); }); }); diff --git a/frontend/test/metabase/public/public.e2e.spec.js b/frontend/test/metabase/public/public.e2e.spec.js index 88d365a56b180c1f3f10f46cf1b34b63c2efbded..13fade435356052ebea4c7eafef85983f577b7bf 100644 --- a/frontend/test/metabase/public/public.e2e.spec.js +++ b/frontend/test/metabase/public/public.e2e.spec.js @@ -37,6 +37,7 @@ import { UPDATE_EMBEDDING_PARAMS, UPDATE_ENABLE_EMBEDDING, UPDATE_TEMPLATE_TAG, + SET_IS_SHOWING_TEMPLATE_TAGS_EDITOR, } from "metabase/query_builder/actions"; import NativeQueryEditor from "metabase/query_builder/components/NativeQueryEditor"; import { delay } from "metabase/lib/promise"; @@ -157,8 +158,11 @@ describe("public/embedded", () => { "select count(*) from products where {{category}}", ); + await store.waitForActions([SET_IS_SHOWING_TEMPLATE_TAGS_EDITOR]); const tagEditorSidebar = app.find(TagEditorSidebar); + click(tagEditorSidebar.find("SelectButton")); + const fieldFilterVarType = tagEditorSidebar .find(".ColumnarSelector-row") .at(3);