diff --git a/frontend/src/metabase/dashboard/actions.js b/frontend/src/metabase/dashboard/actions.js index d440abf492877a3dcb1141c04e599078ba892927..c2ab2b00892911b575ffdaac66d1795b89c3468d 100644 --- a/frontend/src/metabase/dashboard/actions.js +++ b/frontend/src/metabase/dashboard/actions.js @@ -29,7 +29,6 @@ import { getParameterValuesBySlug, getParameterValuesByIdFromQueryParams, } from "metabase/parameters/utils/parameter-values"; -import * as Urls from "metabase/lib/urls"; import { SIDEBAR_NAME } from "metabase/dashboard/constants"; import Utils from "metabase/lib/utils"; @@ -1070,15 +1069,14 @@ export const navigateToNewCardFromDashboard = createThunkAction( const isDrillingFromNativeModel = previousQuestion.isDataset() && previousQuestion.isNative(); - // when the query is for a specific object (ie `=` filter on PK column) - // it does not make sense to apply parameter filters - // because we'll be navigating to the details view of a specific row on a table - const url = question.isObjectDetail() - ? Urls.serializedQuestion(question.card()) - : question.getUrlWithParameters(parametersMappedToCard, parameterValues, { - clean: !isDrillingFromNativeModel, - objectId, - }); + const url = question.getUrlWithParameters( + parametersMappedToCard, + parameterValues, + { + clean: !isDrillingFromNativeModel, + objectId, + }, + ); dispatch(openUrl(url)); }, diff --git a/frontend/src/metabase/modes/components/drill/ObjectDetailDrill.jsx b/frontend/src/metabase/modes/components/drill/ObjectDetailDrill.jsx index a0a348e644354a7e1dedb726ed2a61fbf811b4f3..9f1a194e18db6a6ad820780b4809069dcb49a7bb 100644 --- a/frontend/src/metabase/modes/components/drill/ObjectDetailDrill.jsx +++ b/frontend/src/metabase/modes/components/drill/ObjectDetailDrill.jsx @@ -1,6 +1,5 @@ import { t } from "ttag"; import { isFK, isPK } from "metabase/lib/schema_metadata"; -import * as Urls from "metabase/lib/urls"; import { zoomInRow } from "metabase/query_builder/actions"; function hasManyPKColumns(question) { @@ -11,26 +10,14 @@ function hasManyPKColumns(question) { function getActionForPKColumn({ question, column, objectId, extraData }) { if (hasManyPKColumns(question)) { - // Filter by a clicked value, then a user can click on the 2nd, 3d, ..., Nth PK cells - // to narrow down filtering and eventually enter the object detail view once all PKs are filtered return ["question", () => question.filter("=", column, objectId)]; } const isDashboard = !!extraData?.dashboard; - if (isDashboard) { - const { parameterValuesBySlug = {} } = extraData; - const hasParameters = Object.keys(parameterValuesBySlug).length > 0; - - // This should result in a metabase/dashboard/actions navigateToNewCardFromDashboard call - // That will convert dashboard parameters into question filters - // and make sure the clicked row will be present in the query results - if (hasParameters) { - const getNextQuestion = () => question; - const getExtraData = () => ({ objectId }); - return ["question", getNextQuestion, getExtraData]; - } - return ["url", () => Urls.question(question.card(), { objectId })]; + // the question from the dashboard may have filters applied already + if (isDashboard) { + return ["question", () => question]; } return ["action", () => zoomInRow({ objectId })]; @@ -49,16 +36,13 @@ function getBaseActionObject() { function getPKAction({ question, column, objectId, extraData }) { const actionObject = getBaseActionObject(); - const [actionKey, action, extra] = getActionForPKColumn({ + const [actionKey, action] = getActionForPKColumn({ question, column, objectId, extraData, }); actionObject[actionKey] = action; - if (extra) { - actionObject.extra = extra; - } return actionObject; } @@ -96,5 +80,8 @@ export default ({ question, clicked }) => { const { column, value: objectId, extraData } = clicked; const params = { question, column, objectId, extraData }; const actionObject = isPK(column) ? getPKAction(params) : getFKAction(params); + if (!hasManyPKColumns(question)) { + actionObject.extra = () => ({ objectId }); + } return actionObject ? [actionObject] : []; }; diff --git a/frontend/src/metabase/modes/lib/modes.js b/frontend/src/metabase/modes/lib/modes.js index 1974fa35cd65b7b28ab7bfc21a17a3185f182346..73cb7f321cbc8e1e91d7536397890372fdd3d78c 100644 --- a/frontend/src/metabase/modes/lib/modes.js +++ b/frontend/src/metabase/modes/lib/modes.js @@ -1,4 +1,3 @@ -import ObjectMode from "../components/modes/ObjectMode"; import SegmentMode from "../components/modes/SegmentMode"; import MetricMode from "../components/modes/MetricMode"; import TimeseriesMode from "../components/modes/TimeseriesMode"; @@ -10,23 +9,6 @@ import DefaultMode from "../components/modes/DefaultMode"; import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery"; import NativeQuery from "metabase-lib/lib/queries/NativeQuery"; -const isPKFilter = (filters, query) => { - const sourceTablePKFields = - query?.table()?.fields.filter(field => field.isPK()) || []; - - if (sourceTablePKFields.length === 0) { - return false; - } - - const hasEqualityFilterForEveryPK = sourceTablePKFields.every(pkField => { - const filter = filters.find(filter => filter.field()?.id === pkField.id); - - return filter?.operatorName() === "=" && filter?.arguments().length === 1; - }); - - return hasEqualityFilterForEveryPK; -}; - export function getMode(question) { if (!question) { return null; @@ -41,14 +23,9 @@ export function getMode(question) { if (query instanceof StructuredQuery) { const aggregations = query.aggregations(); const breakouts = query.breakouts(); - const filters = query.filters(); if (aggregations.length === 0 && breakouts.length === 0) { - if (isPKFilter(filters, query)) { - return ObjectMode; - } else { - return SegmentMode; - } + return SegmentMode; } if (aggregations.length > 0 && breakouts.length === 0) { return MetricMode; diff --git a/frontend/src/metabase/query_builder/actions.js b/frontend/src/metabase/query_builder/actions.js index 3b054fd0c0422d76ba051812e2c7675899cb75ee..952b309186b53dfd2643c890598f76811c4c2e9a 100644 --- a/frontend/src/metabase/query_builder/actions.js +++ b/frontend/src/metabase/query_builder/actions.js @@ -1019,7 +1019,7 @@ export const setCardAndRun = (nextCard, shouldUpdateUrl = true) => { export const NAVIGATE_TO_NEW_CARD = "metabase/qb/NAVIGATE_TO_NEW_CARD"; export const navigateToNewCardInsideQB = createThunkAction( NAVIGATE_TO_NEW_CARD, - ({ nextCard, previousCard }) => { + ({ nextCard, previousCard, objectId }) => { return async (dispatch, getState) => { if (previousCard === nextCard) { // Do not reload questions with breakouts when clicked on a legend item @@ -1041,6 +1041,9 @@ export const navigateToNewCardInsideQB = createThunkAction( // to start building a new ad-hoc question based on a dataset dispatch(setCardAndRun({ ...card, dataset: false })); } + if (objectId !== undefined) { + dispatch(zoomInRow({ objectId })); + } } }; }, diff --git a/frontend/src/metabase/query_builder/components/view/QuestionDataSource.unit.spec.js b/frontend/src/metabase/query_builder/components/view/QuestionDataSource.unit.spec.js index d70020c4263e140dd02b64e5f4adbf1dd95e580d..c77d961ef46ffdfa7de73f3fdbdc88bf37d8c099 100644 --- a/frontend/src/metabase/query_builder/components/view/QuestionDataSource.unit.spec.js +++ b/frontend/src/metabase/query_builder/components/view/QuestionDataSource.unit.spec.js @@ -503,7 +503,10 @@ describe("QuestionDataSource", () => { }); }); - describe("Object Detail", () => { + describe.skip("Object Detail", () => { + // these tests do not apply to the new modal object detail view + // but will be useful when we implement the new version of full page + // object detail [ GUI_TEST_CASE.SAVED_OBJECT_DETAIL, GUI_TEST_CASE.AD_HOC_OBJECT_DETAIL, diff --git a/frontend/src/metabase/query_builder/selectors.js b/frontend/src/metabase/query_builder/selectors.js index 924d9365f12041d906f58e1b9d59afe4479a517b..444b9ef68d565a358d2574c618d196b7c943310a 100644 --- a/frontend/src/metabase/query_builder/selectors.js +++ b/frontend/src/metabase/query_builder/selectors.js @@ -479,15 +479,8 @@ export const getMode = createSelector( ); export const getIsObjectDetail = createSelector( - [getMode, getQueryResults, isZoomingRow], - (mode, results, isZoomingSingleRow) => { - if (isZoomingSingleRow) { - return true; - } - // It handles filtering by a manually set PK column that is not unique - const hasMultipleRows = results?.some(({ data }) => data?.rows.length > 1); - return mode?.name() === "object" && !hasMultipleRows; - }, + [getMode, isZoomingRow], + (mode, isZoomingSingleRow) => isZoomingSingleRow || mode?.name() === "object", ); export const getIsDirty = createSelector( diff --git a/frontend/src/metabase/visualizations/components/ObjectDetail/ObjectDetail.tsx b/frontend/src/metabase/visualizations/components/ObjectDetail/ObjectDetail.tsx index 903e4d8dde209e441459967a21b58f63048d78ed..d946119d381602666f8e62d380a6f7bb27aa71e2 100644 --- a/frontend/src/metabase/visualizations/components/ObjectDetail/ObjectDetail.tsx +++ b/frontend/src/metabase/visualizations/components/ObjectDetail/ObjectDetail.tsx @@ -233,7 +233,7 @@ export function ObjectDetailFn({ ) : ( <div className="ObjectDetail" data-testid="object-detail"> <ObjectDetailHeader - canZoom={canZoom} + canZoom={canZoom && (canZoomNextRow || canZoomPreviousRow)} objectName={objectName} objectId={zoomedRowID} canZoomPreviousRow={canZoomPreviousRow} @@ -317,7 +317,6 @@ export function ObjectDetailHeader({ data-testId="object-detail-close-button" onlyIcon borderless - disabled={!canZoomNextRow} onClick={closeObjectDetail} icon="close" iconSize={20} diff --git a/frontend/src/metabase/visualizations/components/TableInteractive.jsx b/frontend/src/metabase/visualizations/components/TableInteractive.jsx index f4dc5a11bb6e62b1402483cf6ac1fc50a4eb11ae..898607c3bbf1b9be979ded2cda846c46ecd45e84 100644 --- a/frontend/src/metabase/visualizations/components/TableInteractive.jsx +++ b/frontend/src/metabase/visualizations/components/TableInteractive.jsx @@ -149,9 +149,10 @@ export default class TableInteractive extends Component { } _findIDColumn = (data, isPivoted = false) => { - const pkIndex = isPivoted - ? -1 - : data.cols.findIndex(col => isPK(col) && col.id !== undefined); + const hasManyPKColumns = data.cols.filter(isPK).length > 1; + + const pkIndex = + isPivoted || hasManyPKColumns ? -1 : data.cols.findIndex(isPK); this.setState({ IDColumnIndex: pkIndex === -1 ? null : pkIndex, diff --git a/frontend/src/metabase/visualizations/lib/action.js b/frontend/src/metabase/visualizations/lib/action.js index 6c2b71f115b3eae6e8e573fa1d01526fe48ef9a7..5cc834e24ac58c9ea9464b45dc5a758f211daf06 100644 --- a/frontend/src/metabase/visualizations/lib/action.js +++ b/frontend/src/metabase/visualizations/lib/action.js @@ -23,7 +23,11 @@ export function performAction(action, { dispatch, onChangeCardAndRun }) { const question = action.question(); const extra = action?.extra?.() ?? {}; if (question) { - onChangeCardAndRun({ nextCard: question.card(), ...extra }); + onChangeCardAndRun({ + nextCard: question.card(), + ...extra, + objectId: extra.objectId, + }); didPerform = true; } } diff --git a/frontend/test/metabase-lib/lib/Mode.unit.spec.js b/frontend/test/metabase-lib/lib/Mode.unit.spec.js index 5691f8200c15e283f1fcc5a599ac501cfda3e1e9..e25dcd9bd73db4b217d2b6532f1e028521be27a0 100644 --- a/frontend/test/metabase-lib/lib/Mode.unit.spec.js +++ b/frontend/test/metabase-lib/lib/Mode.unit.spec.js @@ -82,12 +82,12 @@ describe("Mode", () => { expect(mode && mode.name()).toEqual("pivot"); }); - it("returns `object` mode with pk filter", () => { + it("returns `segment` mode with pk filter", () => { const mode = rawDataQuery .filter(["=", ["field", ORDERS.ID.id, null], 42]) .question() .mode(); - expect(mode && mode.name()).toEqual("object"); + expect(mode && mode.name()).toEqual("segment"); }); it("returns `default` mode with >=0 aggregations and >=3 breakouts", () => { diff --git a/frontend/test/metabase/modes/components/drill/ObjectDetailDrill.unit.spec.js b/frontend/test/metabase/modes/components/drill/ObjectDetailDrill.unit.spec.js index bdcc96d8305a3c3fabbbc968ac420165fbfd0fef..adcd2867c8aed69e05157b31814fc7d5193a66c5 100644 --- a/frontend/test/metabase/modes/components/drill/ObjectDetailDrill.unit.spec.js +++ b/frontend/test/metabase/modes/components/drill/ObjectDetailDrill.unit.spec.js @@ -179,7 +179,7 @@ describe("ObjectDetailDrill", () => { describe("from dashboard", () => { describe("without parameters", () => { - const { actions, cellValue } = setup({ + const { actions } = setup({ question: SAVED_QUESTION, column: ORDERS.ID.column(), extraData: { @@ -191,15 +191,15 @@ describe("ObjectDetailDrill", () => { expect(actions).toMatchObject([ { name: "object-detail", - url: expect.any(Function), + question: expect.any(Function), }, ]); }); it("should return correct URL to object detail", () => { const [action] = actions; - expect(action.url()).toBe( - `/question/${SAVED_QUESTION.id()}-${SAVED_QUESTION.displayName()}/${cellValue}`, + expect(action.question().getUrl()).toBe( + `/question/${SAVED_QUESTION.id()}-${SAVED_QUESTION.displayName()}`, ); }); }); @@ -230,7 +230,7 @@ describe("ObjectDetailDrill", () => { it("should return correct action", () => { const [action] = actions; expect(action.question()).toBe(SAVED_QUESTION); - expect(action.extra()).toEqual({ objectId: cellValue }); + expect(action.extra().objectId).toEqual(cellValue); }); }); }); @@ -255,6 +255,11 @@ describe("ObjectDetailDrill", () => { filter: ["=", PRODUCTS.ID.reference(), cellValue], }); }); + + it("should supply the foreign key as a return value from the extra() function", () => { + const [action] = actions; + expect(action.extra().objectId).toEqual(cellValue); + }); }); describe("with fk_target_field_id (model with customized metadata)", () => { @@ -279,6 +284,10 @@ describe("ObjectDetailDrill", () => { filter: ["=", PRODUCTS.ID.reference(), cellValue], }); }); + it("should supply the foreign key as a return value from the extra() function", () => { + const [action] = actions; + expect(action.extra().objectId).toEqual(cellValue); + }); }); }); }); diff --git a/frontend/test/metabase/modes/lib/modes.unit.spec.js b/frontend/test/metabase/modes/lib/modes.unit.spec.js index 1baa9dc7e9f3ee39773df1aea87271d7a31faa5c..681dc2ce81306f14dc46634dd5d2672a83344885 100644 --- a/frontend/test/metabase/modes/lib/modes.unit.spec.js +++ b/frontend/test/metabase/modes/lib/modes.unit.spec.js @@ -1,16 +1,15 @@ import { getMode } from "metabase/modes/lib/modes"; -import ObjectMode from "metabase/modes/components/modes/ObjectMode"; import SegmentMode from "metabase/modes/components/modes/SegmentMode"; import { ORDERS } from "__support__/sample_database_fixture"; describe("modes", () => { describe("getMode", () => { - it("should be in object mode when selecting one PK ID", () => { + it("should be in segment mode when selecting one PK ID", () => { const filter = ["=", ["field", ORDERS.ID.id, null], 42]; const query = ORDERS.query().filter(filter); const question = ORDERS.question().setQuery(query); - expect(getMode(question)).toBe(ObjectMode); + expect(getMode(question)).toBe(SegmentMode); }); it("should be in segment mode when selecting multiple PK IDs", () => { const filter = ["=", ["field", ORDERS.ID.id, null], 42, 24]; diff --git a/frontend/test/metabase/scenarios/models/models-metadata.cy.spec.js b/frontend/test/metabase/scenarios/models/models-metadata.cy.spec.js index fc39ad08e83649fc9431003e824b844b04d85c99..c4113d40b4ff12468b5670bbc7fd8cd67130c937 100644 --- a/frontend/test/metabase/scenarios/models/models-metadata.cy.spec.js +++ b/frontend/test/metabase/scenarios/models/models-metadata.cy.spec.js @@ -222,7 +222,9 @@ describe("scenarios > models metadata", () => { cy.findByText("Hudson Borer"); }); - cy.go("back"); + cy.go("back"); // close modal + cy.wait("@dataset"); + cy.go("back"); // navigate away from drilled table cy.wait("@dataset"); // Drill to Reviews table diff --git a/frontend/test/metabase/scenarios/question/new.cy.spec.js b/frontend/test/metabase/scenarios/question/new.cy.spec.js index d1f31391add0605a058fba8b6fed4a62a2614934..9a08d9e684c93109d1264e6b68ea7a4ce302bf61 100644 --- a/frontend/test/metabase/scenarios/question/new.cy.spec.js +++ b/frontend/test/metabase/scenarios/question/new.cy.spec.js @@ -230,7 +230,7 @@ describe("scenarios > question > new", () => { cy.url().should("include", "question#"); }); - it("should correctly choose between 'Object Detail' and 'Table (metabase#13717)", () => { + it("composite keys should act as filters on click (metabase#13717)", () => { cy.request("PUT", `/api/field/${ORDERS.QUANTITY}`, { semantic_type: "type/PK", }); @@ -241,6 +241,11 @@ describe("scenarios > question > new", () => { .eq(1) // first table body cell .should("contain", "2") // quantity for order ID#1 .click(); + cy.wait("@dataset"); + + cy.get( + "#main-data-grid .TableInteractive-cellWrapper--firstColumn", + ).should("have.length.gt", 1); cy.log( "**Reported at v0.34.3 - v0.37.0.2 / probably was always like this**", @@ -254,8 +259,12 @@ describe("scenarios > question > new", () => { .eq(1) // first table body cell .should("contain", 1) .click(); + cy.wait("@dataset"); - cy.get(".ObjectDetail"); + cy.log("only one row should appear after filtering by ID"); + cy.get( + "#main-data-grid .TableInteractive-cellWrapper--firstColumn", + ).should("have.length", 1); }); // flaky test (#19454) diff --git a/frontend/test/metabase/scenarios/question/nulls.cy.spec.js b/frontend/test/metabase/scenarios/question/nulls.cy.spec.js index a321d93da7222035055e4c2f7e3defafb64a72f8..9bac164114631f2fc81ccb22a3753e8e7c6d1078 100644 --- a/frontend/test/metabase/scenarios/question/nulls.cy.spec.js +++ b/frontend/test/metabase/scenarios/question/nulls.cy.spec.js @@ -22,7 +22,10 @@ describe("scenarios > question > null", () => { name: "13571", query: { "source-table": ORDERS_ID, - fields: [["field", ORDERS.DISCOUNT, null]], + fields: [ + ["field", ORDERS.ID, null], + ["field", ORDERS.DISCOUNT, null], + ], filter: ["=", ["field", ORDERS.ID, null], 1], }, }); @@ -32,6 +35,7 @@ describe("scenarios > question > null", () => { cy.findByText("13571").click(); cy.log("'No Results since at least v0.34.3"); + cy.get("#detail-shortcut").click(); cy.findByText("Discount"); cy.findByText("Empty"); }); diff --git a/frontend/test/metabase/scenarios/question/reproductions/17514-ui-overlay.cy.spec.js b/frontend/test/metabase/scenarios/question/reproductions/17514-ui-overlay.cy.spec.js index f1c788719322da79af1b4bbbd2f4a8b6931be603..3356d9f746299b8d6f133bad5a3de2269b7efcb7 100644 --- a/frontend/test/metabase/scenarios/question/reproductions/17514-ui-overlay.cy.spec.js +++ b/frontend/test/metabase/scenarios/question/reproductions/17514-ui-overlay.cy.spec.js @@ -140,11 +140,10 @@ describe("issue 17514", () => { cy.findByText("Products").click(); visualize(); - cy.findByTextEnsureVisible("Subtotal"); // Cypress cannot click elements that are blocked by an overlay so this will immediately fail if the issue is not fixed - cy.findByText("110.93").click(); - cy.findByText("Filter by this value"); + cy.findByTextEnsureVisible("Subtotal").click(); + cy.findByText("Filter by this column"); }); }); }); diff --git a/frontend/test/metabase/scenarios/visualizations/object_detail.cy.spec.js b/frontend/test/metabase/scenarios/visualizations/object_detail.cy.spec.js index 94ed50cb48b10ddcfd9e73b31f242a6b66df71d8..fea720d7c51bcf9803e1eef3233b981bbd6e5f19 100644 --- a/frontend/test/metabase/scenarios/visualizations/object_detail.cy.spec.js +++ b/frontend/test/metabase/scenarios/visualizations/object_detail.cy.spec.js @@ -62,6 +62,7 @@ describe("scenarios > question > object details", () => { getPreviousObjectDetailButton().should("not.exist"); getNextObjectDetailButton().should("not.exist"); + cy.go("back"); cy.go("back"); cy.wait("@dataset");