diff --git a/frontend/src/metabase-lib/lib/Question.ts b/frontend/src/metabase-lib/lib/Question.ts index e5125faa1c33b47de273830f5924087af1a29175..2e992561b9542e5308eb8d43f4d07ee60c4d613f 100644 --- a/frontend/src/metabase-lib/lib/Question.ts +++ b/frontend/src/metabase-lib/lib/Question.ts @@ -1355,6 +1355,10 @@ export default class Question extends memoizeClass<QuestionInner>( dataset_query, }; + if (type === "native") { + card = assocIn(card, ["parameters"], []); + } + if (tableId != null) { card = assocIn(card, ["dataset_query", "query", "source-table"], tableId); } diff --git a/frontend/src/metabase/query_builder/actions/core/core.js b/frontend/src/metabase/query_builder/actions/core/core.js index 704e348cbbb3d2d58747291912a2b251f8a3f5e1..eb8d157ae1d89e6eadf076cce3cc2905462b4caa 100644 --- a/frontend/src/metabase/query_builder/actions/core/core.js +++ b/frontend/src/metabase/query_builder/actions/core/core.js @@ -26,6 +26,11 @@ import { fetchAlertsForQuestion } from "metabase/alert/alert"; import Question from "metabase-lib/lib/Question"; import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery"; +import { + getTemplateTagsForParameters, + getTemplateTagParameters, +} from "metabase/parameters/utils/cards"; + import { trackNewQuestionSaved } from "../../analytics"; import { getCard, @@ -307,6 +312,14 @@ export const updateQuestion = ( ); } + const newDatasetQuery = newQuestion.query().datasetQuery(); + // Sync card's parameters with the template tags; + if (newDatasetQuery.type === "native") { + const templateTags = getTemplateTagsForParameters(newQuestion.card()); + const parameters = getTemplateTagParameters(templateTags); + newQuestion = newQuestion.setParameters(parameters); + } + // Replace the current question with a new one await dispatch.action(UPDATE_QUESTION, { card: newQuestion.card() }); diff --git a/frontend/src/metabase/query_builder/actions/native.js b/frontend/src/metabase/query_builder/actions/native.js index 7933655c22eb198003d08e2b87109d66923744ee..57b3622c6f0b66103b2ba9b16b7ef25e3738d6bc 100644 --- a/frontend/src/metabase/query_builder/actions/native.js +++ b/frontend/src/metabase/query_builder/actions/native.js @@ -20,7 +20,6 @@ import { SET_UI_CONTROLS } from "./ui"; import { getTemplateTagsForParameters, getTemplateTagParameters, - getTemplateTagParameter, } from "metabase/parameters/utils/cards"; export const TOGGLE_DATA_REFERENCE = "metabase/qb/TOGGLE_DATA_REFERENCE"; @@ -144,29 +143,11 @@ export const setTemplateTag = createThunkAction( }, ); - const { parameters } = updatedTagsCard; - if (parameters && Array.isArray(parameters)) { - if (parameters.length === 0) { - // reconstruct from the existing template tags - const tags = getTemplateTagsForParameters(updatedTagsCard); - const newParameters = getTemplateTagParameters(tags); - return assoc(updatedTagsCard, "parameters", newParameters); - } else { - // update an existing parameter - const index = parameters.findIndex(p => p.id === templateTag.id); - if (index < 0) { - console.warn(`Can't find parameter with id=${templateTag.id}!`); - } else { - parameters[index] = getTemplateTagParameter(templateTag); - } - } - } else { - const tags = getTemplateTagsForParameters(updatedTagsCard); - const newParameters = getTemplateTagParameters(tags); - return assoc(updatedTagsCard, "parameters", newParameters); - } - - return updatedTagsCard; + return assoc( + updatedTagsCard, + "parameters", + getTemplateTagParameters(getTemplateTagsForParameters(updatedTagsCard)), + ); }; }, ); diff --git a/frontend/src/metabase/query_builder/components/template_tags/TagEditorSidebar.jsx b/frontend/src/metabase/query_builder/components/template_tags/TagEditorSidebar.jsx index 2d22ece69583dd4f633511261d563dd9baf1926f..7ea25645969b43d64c86be52e729ae096616f444 100644 --- a/frontend/src/metabase/query_builder/components/template_tags/TagEditorSidebar.jsx +++ b/frontend/src/metabase/query_builder/components/template_tags/TagEditorSidebar.jsx @@ -50,6 +50,7 @@ export default class TagEditorSidebar extends React.Component { setParameterValue, onClose, } = this.props; + // The tag editor sidebar excludes snippets since they have a separate sidebar. const tags = query.templateTagsWithoutSnippets(); const database = query.database(); diff --git a/frontend/test/metabase/scenarios/native/native.cy.spec.js b/frontend/test/metabase/scenarios/native/native.cy.spec.js index 28bc2fe850262668d08b1c2b3cce60fe56b35903..178d34ce886b9acfe7939cca320ead8db7518de2 100644 --- a/frontend/test/metabase/scenarios/native/native.cy.spec.js +++ b/frontend/test/metabase/scenarios/native/native.cy.spec.js @@ -216,6 +216,42 @@ describe("scenarios > question > native", () => { cy.get("@sidebar").contains(/added/i); }); + it("should recognize template tags and save them as parameters", () => { + openNativeEditor().type( + "select * from PRODUCTS where CATEGORY={{cat}} and RATING >= {{stars}}", + { + parseSpecialCharSequences: false, + }, + ); + cy.get("input[placeholder*='Cat']").type("Gizmo"); + cy.get("input[placeholder*='Stars']").type("3"); + + cy.get(".NativeQueryEditor .Icon-play").click(); + cy.wait("@dataset"); + + cy.contains("Save").click(); + + modal().within(() => { + cy.findByLabelText("Name").type("SQL Products"); + cy.findByText("Save").click(); + + // parameters[] should reflect the template tags + cy.wait("@card").should(xhr => { + const requestBody = xhr.request?.body; + expect(requestBody?.parameters?.length).to.equal(2); + }); + }); + cy.findByText("Not now").click(); + + // Now load the question again and parameters[] should still be there + cy.intercept("GET", "/api/card/4").as("cardQuestion"); + cy.visit("/question/4?cat=Gizmo&stars=3"); + cy.wait("@cardQuestion").should(xhr => { + const responseBody = xhr.response?.body; + expect(responseBody?.parameters?.length).to.equal(2); + }); + }); + it("should link correctly from the variables sidebar (metabase#16212)", () => { cy.createNativeQuestion({ name: "test-question",