Skip to content
Snippets Groups Projects
Unverified Commit 63833f46 authored by github-automation-metabase's avatar github-automation-metabase Committed by GitHub
Browse files

:robot: backported "feat(sdk): combine title props in interactive question" (#51066)

parent 9b0b0cff
No related branches found
No related tags found
No related merge requests found
Showing with 84 additions and 23 deletions
......@@ -69,19 +69,18 @@ export default function App() {
## Question props
| Prop | Type | Description |
|-----------------------|----------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| --------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| questionId | number or string | (required) The ID of the question. This is either:<br>- The numerical ID when accessing a question link, e.g., `http://localhost:3000/question/1-my-question` where the ID is `1`.<br>- The `entity_id` key of the question object. You can find a question's entity ID in the info panel when viewing a question. |
| plugins | `{ mapQuestionClickActions: Function }` or null | Additional mapper function to override or add drill-down menu. |
| height | number or string | (optional) A number or string specifying a CSS size value that specifies the height of the component |
| entityTypeFilter | string array; options include "table", "question", "model", "metric" | (optional) An array that specifies which entity types are available in the data picker |
| isSaveEnabled | boolean | (optional) Whether people can save the question. |
| withResetButton | boolean | (optional, default: `true`) Determines whether a reset button is displayed. Only relevant when using the default layout |
| withTitle | boolean | (optional, default: `false`) Determines whether the question title is displayed. Only relevant when using the default layout. |
| customTitle | string or undefined | (optional) Allows a custom title to be displayed instead of the default question title. Only relevant when using the default layout. |
| withChartTypeSelector | boolean | (optional, default: `true`) Determines whether the chart type selector is shown. Only relevant when using the default layout. |
| title | boolean or string or `ReactNode` or `() => ReactNode` | (optional) Determines whether the question title is displayed, and allows a custom title to be displayed instead of the default question title. Shown by default. Only Only applicable to interactive questions when using the default layout. |
| onBeforeSave | `() => void` | (optional) A callback function that triggers before saving. Only relevant when `isSaveEnabled = true`. |
| onSave | `() => void` | (optional) A callback function that triggers when a user saves the question. Only relevant when `isSaveEnabled = true`. |
| saveToCollectionId | number | (optional) The target collection to save the question to. This will hide the collection picker from the save modal. Only applicable to static questions. |
| saveToCollectionId | number | (optional) The target collection to save the question to. This will hide the collection picker from the save modal. Only applicable to interactive questions. |
## Customizing interactive questions
......
import { type ReactNode, useMemo } from "react";
import type { SdkPluginsConfig } from "embedding-sdk";
import type { SdkQuestionTitleProps } from "embedding-sdk/types/question";
import {
InteractiveQuestionProviderWithLocation,
......@@ -11,8 +12,7 @@ import { InteractiveQuestionResult } from "./InteractiveQuestionResult";
interface InteractiveAdHocQuestionProps {
questionPath: string; // route path to load a question, e.g. /question/140-best-selling-products - for saved, or /question/xxxxxxx for ad-hoc encoded question config
onNavigateBack: () => void;
withTitle?: boolean;
title: SdkQuestionTitleProps;
height?: number;
plugins?: SdkPluginsConfig;
children?: ReactNode;
......@@ -21,7 +21,7 @@ interface InteractiveAdHocQuestionProps {
export const InteractiveAdHocQuestion = ({
questionPath,
onNavigateBack,
withTitle = true,
title = true,
height,
plugins,
children,
......@@ -41,7 +41,7 @@ export const InteractiveAdHocQuestion = ({
{children ?? (
<InteractiveQuestionResult
height={height}
withTitle={withTitle}
title={title}
withChartTypeSelector
/>
)}
......
import { useDisclosure } from "@mantine/hooks";
import cx from "classnames";
import { type ReactElement, type ReactNode, useState } from "react";
import { type ReactElement, type ReactNode, useMemo, useState } from "react";
import { match } from "ts-pattern";
import { t } from "ttag";
......@@ -8,6 +8,7 @@ import {
SdkError,
SdkLoader,
} from "embedding-sdk/components/private/PublicComponentWrapper";
import type { SdkQuestionTitleProps } from "embedding-sdk/types/question";
import { SaveQuestionModal } from "metabase/containers/SaveQuestionModal";
import { Box, Button, Group, Icon } from "metabase/ui";
......@@ -21,9 +22,8 @@ import { useInteractiveQuestionContext } from "../InteractiveQuestion/context";
import InteractiveQuestionS from "./InteractiveQuestionResult.module.css";
export interface InteractiveQuestionResultProps {
title?: SdkQuestionTitleProps;
withResetButton?: boolean;
withTitle?: boolean;
customTitle?: ReactNode;
withChartTypeSelector?: boolean;
}
......@@ -55,8 +55,7 @@ export const InteractiveQuestionResult = ({
width,
className,
style,
withTitle,
customTitle,
title,
withResetButton,
withChartTypeSelector,
}: InteractiveQuestionResultProps & FlexibleSizeProps): ReactElement => {
......@@ -83,6 +82,25 @@ export const InteractiveQuestionResult = ({
// When visualizing a question for the first time, there is no query result yet.
const isQueryResultLoading = question && !queryResults;
const questionTitleElement: ReactNode = useMemo(() => {
if (title === false) {
return null;
}
if (title === undefined || title === true) {
return <InteractiveQuestion.Title />;
}
if (typeof title === "function") {
const CustomTitle = title;
// TODO: pass in question={question} once we have the public-facing question type (metabase#50487)
return <CustomTitle />;
}
return title;
}, [title]);
if (isQuestionLoading || isQueryResultLoading) {
return <SdkLoader />;
}
......@@ -100,7 +118,7 @@ export const InteractiveQuestionResult = ({
>
<Group className={InteractiveQuestionS.TopBar} position="apart" p="md">
<InteractiveQuestion.BackButton />
{withTitle && (customTitle ?? <InteractiveQuestion.Title />)}
{questionTitleElement}
<Group spacing="xs">
{withResetButton && <InteractiveQuestion.ResetButton />}
<InteractiveQuestion.FilterButton
......
......@@ -72,7 +72,7 @@ export const EditableDashboard = ({
{adhocQuestionUrl ? (
<InteractiveAdHocQuestion
questionPath={adhocQuestionUrl}
withTitle
title={true}
height={drillThroughQuestionHeight}
plugins={plugins}
onNavigateBack={onNavigateBackToDashboard}
......
......@@ -85,7 +85,7 @@ const InteractiveDashboardInner = ({
{adhocQuestionUrl ? (
<InteractiveAdHocQuestion
questionPath={adhocQuestionUrl}
withTitle={withTitle}
title={withTitle}
height={drillThroughQuestionHeight}
plugins={plugins}
onNavigateBack={onNavigateBackToDashboard}
......
......@@ -46,8 +46,7 @@ export type InteractiveQuestionProps = PropsWithChildren<{
export const _InteractiveQuestion = ({
questionId,
withResetButton = true,
withTitle = false,
customTitle,
title,
plugins,
height,
width,
......@@ -78,9 +77,8 @@ export const _InteractiveQuestion = ({
width={width}
className={className}
style={style}
customTitle={customTitle}
title={title}
withResetButton={withResetButton}
withTitle={withTitle}
withChartTypeSelector={withChartTypeSelector}
/>
)}
......
......@@ -18,6 +18,7 @@ import {
import { InteractiveQuestionResult } from "embedding-sdk/components/private/InteractiveQuestionResult";
import { createMockAuthProviderUriConfig } from "embedding-sdk/test/mocks/config";
import { setupSdkState } from "embedding-sdk/test/server-mocks/sdk-init";
import type { SdkQuestionTitleProps } from "embedding-sdk/types/question";
import {
createMockCard,
createMockCardQueryMetadata,
......@@ -53,23 +54,29 @@ const TEST_DATASET = createMockDataset({
});
// Provides a button to re-run the query
function InteractiveQuestionCustomLayout() {
function InteractiveQuestionCustomLayout({
title,
}: {
title?: SdkQuestionTitleProps;
}) {
const { resetQuestion } = useInteractiveQuestionContext();
return (
<div>
<button onClick={resetQuestion}>Run Query</button>
<InteractiveQuestionResult withTitle />
<InteractiveQuestionResult title={title} />
</div>
);
}
const setup = ({
isValidCard = true,
title,
withCustomLayout = false,
withChartTypeSelector = false,
}: {
isValidCard?: boolean;
title?: SdkQuestionTitleProps;
withCustomLayout?: boolean;
withChartTypeSelector?: boolean;
} = {}) => {
......@@ -77,7 +84,7 @@ const setup = ({
currentUser: TEST_USER,
});
const TEST_CARD = createMockCard();
const TEST_CARD = createMockCard({ name: "My Question" });
if (isValidCard) {
setupCardEndpoints(TEST_CARD);
setupCardQueryMetadataEndpoint(
......@@ -99,6 +106,7 @@ const setup = ({
return renderWithProviders(
<InteractiveQuestion
questionId={TEST_CARD.id}
title={title}
withChartTypeSelector={withChartTypeSelector}
>
{withCustomLayout ? <InteractiveQuestionCustomLayout /> : undefined}
......@@ -180,6 +188,35 @@ describe("InteractiveQuestion", () => {
expect(screen.getByText("Question not found")).toBeInTheDocument();
});
it.each([
// shows the question title by default
[undefined, "My Question"],
// hides the question title when title={false}
[false, null],
// shows the default question title when title={true}
[true, "My Question"],
// customizes the question title via strings
["Foo Bar", "Foo Bar"],
// customizes the question title via React elements
[<h1 key="foo">Foo Bar</h1>, "Foo Bar"],
// customizes the question title via React components.
[() => <h1>Foo Bar</h1>, "Foo Bar"],
])(
"shows the question title according to the title prop",
async (titleProp, expectedTitle) => {
setup({ title: titleProp });
await waitForLoaderToBeRemoved();
const element = screen.queryByText(expectedTitle ?? "My Question");
expect(element?.textContent ?? null).toBe(expectedTitle);
},
);
it("should show a chart type selector button if withChartTypeSelector is true", async () => {
setup({ withChartTypeSelector: true });
await waitForLoaderToBeRemoved();
......
import type { ReactNode } from "react";
import type { Deferred } from "metabase/lib/promise";
import type { QueryParams } from "metabase/query_builder/actions";
import type { ObjectId } from "metabase/visualizations/components/ObjectDetail/types";
......@@ -23,3 +25,10 @@ export interface NavigateToNewCardParams {
objectId: ObjectId;
cancelDeferred?: Deferred;
}
export type SdkQuestionTitleProps =
| boolean
| undefined
| ReactNode
// TODO: turn this into (question: Question) => ReactNode once we have the public-facing question type (metabase#50487)
| (() => ReactNode);
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment