Compare commits

...

17 Commits

Author SHA1 Message Date
Oisin Coveney
a6d19c1ff6 Merge branch 'ts-conversion/view-title-header-right-side' into ts-conversion/view-header 2024-06-21 13:11:12 +03:00
Oisin Coveney
2df99c08f3 Delete unused file 2024-06-21 13:06:59 +03:00
Oisin Coveney
a526ec22be Merge branch 'master' into ts-conversion/view-title-header-right-side 2024-06-20 15:04:23 +03:00
Oisin Coveney
3336e7890e Merge branch 'master' into ts-conversion/view-title-header-right-side 2024-06-20 14:20:38 +03:00
Oisin Coveney
42889a0862 Fix lint error 2024-06-20 12:33:33 +03:00
Oisin Coveney
116294ff93 Remove styled and use variant for action icon 2024-06-20 12:20:02 +03:00
Oisin Coveney
5d687f2644 Save changes 2024-06-19 14:45:41 +03:00
Oisin Coveney
390f77f4c9 Convert ViewHeader and expand props 2024-06-19 14:30:36 +03:00
Oisin Coveney
f40f90c0e1 Fix more e2e tests 2024-06-19 13:19:40 +03:00
Oisin Coveney
1526cd63eb Fix fe-unit test 2024-06-19 13:07:45 +03:00
Oisin Coveney
de02006177 Fix tests 2024-06-19 12:22:06 +03:00
Oisin Coveney
36184a4ed4 Remove unneeded extra import path 2024-06-18 18:49:57 +03:00
Oisin Coveney
6c1ae4ae64 Ensure prop validation 2024-06-18 18:47:24 +03:00
Oisin Coveney
5f92ddc8e8 Remove unneeded prop 2024-06-18 18:37:08 +03:00
Oisin Coveney
0e5a89c4d7 Convert ViewTitleHeaderRightSide and associated components 2024-06-18 18:34:08 +03:00
Oisin Coveney
98020dfc7a Convert Dashboard Back Button to TS (#44348) 2024-06-18 13:10:42 +03:00
Oisin Coveney
0f6f5b8b64 Split components into separate files 2024-06-18 12:50:36 +03:00
19 changed files with 468 additions and 323 deletions

View File

@@ -16,6 +16,7 @@ import {
} from "e2e/support/helpers";
import { runQuery } from "../native-filters/helpers/e2e-sql-filter-helpers";
const { PRODUCTS } = SAMPLE_DATABASE;
describe("issue 12439", () => {
@@ -223,8 +224,11 @@ describe("issue 18148", () => {
});
it("should not offer to save the question before it is actually possible to save it (metabase#18148)", () => {
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Save").should("have.attr", "aria-disabled", "true");
cy.findByTestId("qb-save-button").should(
"have.attr",
"aria-disabled",
"true",
);
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Select a database").click();

View File

@@ -210,6 +210,7 @@ describe.skip("issue 17777", () => {
visibility_type: "hidden",
});
}
beforeEach(() => {
restore();
cy.signInAsAdmin();
@@ -267,6 +268,7 @@ describe("issue 19603", () => {
describeEE("issue 20436", () => {
const url = `/admin/permissions/data/group/${ALL_USERS_GROUP}`;
function changePermissions(from, to) {
cy.findAllByText(from).first().click();
@@ -346,11 +348,9 @@ describe("UI elements that make no sense for users without data permissions (met
});
cy.findByTextEnsureVisible("Line options");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Save")
cy.findByTestId("qb-save-button")
.as("saveButton")
.invoke("css", "pointer-events")
.should("equal", "none");
.should("have.attr", "data-disabled");
cy.get("@saveButton").realHover();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
@@ -455,6 +455,7 @@ describeEE("issue 22695 ", () => {
.should("have.length.above", 0)
.and("not.contain", "Sample Database");
}
beforeEach(() => {
cy.intercept("GET", "/api/search?*").as("searchResults");

View File

@@ -1,5 +1,5 @@
import { Component } from "react";
import { connect } from "react-redux";
import { Component, type ComponentType } from "react";
import { connect, type ConnectedProps } from "react-redux";
import { t } from "ttag";
import _ from "underscore";
@@ -14,6 +14,7 @@ import Questions from "metabase/entities/questions";
import { CreateAlertModalContent } from "metabase/query_builder/components/AlertModals";
import { ImpossibleToCreateModelModal } from "metabase/query_builder/components/ImpossibleToCreateModelModal";
import NewDatasetModal from "metabase/query_builder/components/NewDatasetModal";
import type { ModalType } from "metabase/query_builder/components/QueryModals";
import { QuestionEmbedWidget } from "metabase/query_builder/components/QuestionEmbedWidget";
import { PreviewQueryModal } from "metabase/query_builder/components/view/PreviewQueryModal";
import { MODAL_TYPES } from "metabase/query_builder/constants";
@@ -33,13 +34,13 @@ import type {
State,
} from "metabase-types/store";
import type { UpdateQuestionOpts } from "../actions/core/updateQuestion";
import type { UpdateQuestionOpts } from "../../actions/core/updateQuestion";
const mapDispatchToProps = {
setQuestionCollection: Questions.actions.setCollection,
};
const mapStateToProps = (state: State, props: QueryModalsProps) => ({
const mapStateToProps = (state: State, props: OwnProps) => ({
questionWithParameters: getQuestionWithParameters(state) as Question,
initialCollectionId: Collections.selectors.getInitialCollectionId(
state,
@@ -47,20 +48,19 @@ const mapStateToProps = (state: State, props: QueryModalsProps) => ({
),
});
type ModalType = typeof MODAL_TYPES[keyof typeof MODAL_TYPES];
const connector = connect(mapStateToProps, mapDispatchToProps);
type ReduxProps = ConnectedProps<typeof connector>;
interface QueryModalsProps {
type OwnProps = {
questionAlerts: Alert[];
user: User;
modal: ModalType;
modalContext: number;
question: Question;
initialCollectionId: number;
updateQuestion: (question: Question, config?: UpdateQuestionOpts) => void;
setQueryBuilderMode: (mode: QueryBuilderMode) => void;
setUIControls: (opts: Partial<QueryBuilderUIControls>) => void;
originalQuestion: Question;
questionWithParameters: Question;
card: Card;
onCreate: (question: Question) => Promise<void>;
onSave: (
@@ -70,12 +70,9 @@ interface QueryModalsProps {
onCloseModal: () => void;
onOpenModal: (modal: ModalType) => void;
onChangeLocation: (location: string) => void;
setQuestionCollection: (
{ id }: Pick<Card, "id">,
collection: { id: CollectionId },
opts: Record<string, unknown>,
) => void;
}
};
type QueryModalsProps = ReduxProps & OwnProps;
class QueryModals extends Component<QueryModalsProps> {
showAlertsAfterQuestionSaved = () => {
@@ -343,4 +340,4 @@ class QueryModals extends Component<QueryModalsProps> {
}
// eslint-disable-next-line import/no-default-export -- deprecated usage
export default connect(mapStateToProps, mapDispatchToProps)(QueryModals);
export default connector(QueryModals) as ComponentType<OwnProps>;

View File

@@ -0,0 +1,3 @@
// eslint-disable-next-line import/no-default-export
export { default } from "./QueryModals";
export * from "./types";

View File

@@ -0,0 +1,3 @@
import type { MODAL_TYPES } from "metabase/query_builder/constants";
export type ModalType = typeof MODAL_TYPES[keyof typeof MODAL_TYPES];

View File

@@ -1,136 +0,0 @@
import PropTypes from "prop-types";
import { useCallback, useEffect } from "react";
import { usePrevious } from "react-use";
import { useToggle } from "metabase/hooks/use-toggle";
import * as Lib from "metabase-lib";
import { ViewHeaderContainer } from "./ViewHeader.styled";
import {
AdHocQuestionLeftSide,
FilterHeader,
SavedQuestionLeftSide,
ViewTitleHeaderRightSide,
DashboardBackButton,
} from "./components";
const viewTitleHeaderPropTypes = {
question: PropTypes.object.isRequired,
originalQuestion: PropTypes.object,
queryBuilderMode: PropTypes.oneOf(["view", "notebook"]),
setQueryBuilderMode: PropTypes.func,
result: PropTypes.object,
isDirty: PropTypes.bool,
isRunnable: PropTypes.bool,
isRunning: PropTypes.bool,
isResultDirty: PropTypes.bool,
isNativeEditorOpen: PropTypes.bool,
isNavBarOpen: PropTypes.bool,
isShowingSummarySidebar: PropTypes.bool,
isShowingQuestionDetailsSidebar: PropTypes.bool,
isObjectDetail: PropTypes.bool,
isAdditionalInfoVisible: PropTypes.bool,
runQuestionQuery: PropTypes.func,
cancelQuery: PropTypes.func,
updateQuestion: PropTypes.func,
onOpenModal: PropTypes.func,
onEditSummary: PropTypes.func,
onCloseSummary: PropTypes.func,
onOpenQuestionDetails: PropTypes.func,
className: PropTypes.string,
style: PropTypes.object,
};
export function ViewTitleHeader(props) {
const { question, className, style, isNavBarOpen, updateQuestion } = props;
const [
areFiltersExpanded,
{ turnOn: expandFilters, turnOff: collapseFilters },
] = useToggle(!question?.isSaved());
const previousQuestion = usePrevious(question);
const query = question.query();
const previousQuery = usePrevious(query);
useEffect(() => {
const { isNative } = Lib.queryDisplayInfo(query);
const isPreviousQuestionNative =
previousQuery && Lib.queryDisplayInfo(previousQuery).isNative;
if (isNative || isPreviousQuestionNative) {
return;
}
const filtersCount = Lib.filters(query, -1).length;
const previousFiltersCount =
previousQuery && Lib.filters(previousQuery, -1).length;
if (filtersCount > previousFiltersCount) {
expandFilters();
}
}, [previousQuestion, question, expandFilters, previousQuery, query]);
const { isNative } = Lib.queryDisplayInfo(query);
const isSaved = question.isSaved();
const isModelOrMetric =
question.type() === "model" || question.type() === "metric";
const isSummarized = Lib.aggregations(query, -1).length > 0;
const onQueryChange = useCallback(
newQuery => {
updateQuestion(newQuery.question(), { run: true });
},
[updateQuestion],
);
return (
<>
<ViewHeaderContainer
className={className}
style={style}
data-testid="qb-header"
isNavBarOpen={isNavBarOpen}
>
<DashboardBackButton />
{isSaved ? (
<SavedQuestionLeftSide {...props} />
) : (
<AdHocQuestionLeftSide
{...props}
isNative={isNative}
isSummarized={isSummarized}
/>
)}
<ViewTitleHeaderRightSide
{...props}
isSaved={isSaved}
isModelOrMetric={isModelOrMetric}
isNative={isNative}
isSummarized={isSummarized}
areFiltersExpanded={areFiltersExpanded}
onExpandFilters={expandFilters}
onCollapseFilters={collapseFilters}
onQueryChange={onQueryChange}
/>
</ViewHeaderContainer>
{FilterHeader.shouldRender(props) && (
<FilterHeader
{...props}
expanded={areFiltersExpanded}
question={question}
onQueryChange={onQueryChange}
/>
)}
</>
);
}
ViewTitleHeader.propTypes = viewTitleHeaderPropTypes;

View File

@@ -2,10 +2,10 @@ import styled from "@emotion/styled";
import LastEditInfoLabel from "metabase/components/LastEditInfoLabel";
import Button from "metabase/core/components/Button";
import Link from "metabase/core/components/Link";
import { color, alpha } from "metabase/lib/colors";
import { APP_SUBHEADER_HEIGHT } from "metabase/nav/constants";
import { breakpointMaxSmall, space } from "metabase/styled-components/theme";
import { Button as MantineButton, type ButtonProps } from "metabase/ui";
import RunButtonWithTooltip from "../../RunButtonWithTooltip";
import ViewSection, { ViewSubHeading, ViewHeading } from "../ViewSection";
@@ -60,12 +60,12 @@ export const BackButtonContainer = styled.span`
margin-right: 0.75rem;
`;
export const SaveButton = styled(Link)`
color: var(--mb-color-brand);
font-weight: bold;
padding: 0.5rem 1rem;
export const SaveButton = styled(MantineButton)<ButtonProps>`
border-radius: 8px;
background-color: var(--mb-color-bg-white);
&[data-disabled="true"] {
pointer-events: all;
}
:hover {
background-color: var(--mb-color-bg-light);
@@ -78,7 +78,9 @@ export const SavedQuestionHeaderButtonContainer = styled.div<{
right: ${props => (props.isModelOrMetric ? "0px" : "0.38rem")};
`;
export const HeaderButton = styled(Button)<{ active: boolean }>`
export const HeaderButton = styled(Button)<{
active: boolean;
}>`
font-size: 0.875rem;
background-color: ${({ active, color = getDefaultColor() }) =>
active ? color : "transparent"};
@@ -129,7 +131,9 @@ export const AdHocLeftSideRoot = styled.div`
}
`;
export const SavedQuestionLeftSideRoot = styled.div<{ showSubHeader: boolean }>`
export const SavedQuestionLeftSideRoot = styled.div<{
showSubHeader: boolean;
}>`
${SavedQuestionHeaderButtonContainer} {
transition: all 400ms ease;
position: relative;

View File

@@ -0,0 +1,207 @@
import { type CSSProperties, useEffect } from "react";
import { usePrevious } from "react-use";
import { useToggle } from "metabase/hooks/use-toggle";
import type { UpdateQuestionOpts } from "metabase/query_builder/actions";
import type { ModalType } from "metabase/query_builder/components/QueryModals";
import * as Lib from "metabase-lib";
import type Question from "metabase-lib/v1/Question";
import type { Dataset } from "metabase-types/api";
import type { QueryBuilderMode } from "metabase-types/store";
import { ViewHeaderContainer } from "./ViewHeader.styled";
import {
AdHocQuestionLeftSide,
FilterHeader,
SavedQuestionLeftSide,
ViewTitleHeaderRightSide,
DashboardBackButton,
} from "./components";
type ViewTitleHeaderProps = {
question: Question;
result: Dataset | null;
updateQuestion: (question: Question, config?: UpdateQuestionOpts) => void;
originalQuestion: Question;
queryBuilderMode: QueryBuilderMode;
setQueryBuilderMode: (mode: QueryBuilderMode) => void;
isDirty: boolean;
isRunnable: boolean;
isRunning: boolean;
isResultDirty: boolean;
isNativeEditorOpen: boolean;
isNavBarOpen: boolean;
isShowingSummarySidebar: boolean;
isObjectDetail: boolean;
isAdditionalInfoVisible: boolean;
runQuestionQuery: (opts?: {
overrideWithQuestion?: Question;
shouldUpdateUrl?: boolean;
}) => void;
cancelQuery: () => void;
onOpenModal: (modal: ModalType) => void;
onEditSummary: () => void;
onCloseSummary: () => void;
isBookmarked: boolean;
isActionListVisible: boolean;
turnDatasetIntoQuestion: () => void;
toggleBookmark: () => void;
onOpenQuestionInfo: () => void;
onCloseQuestionInfo: () => void;
isShowingQuestionInfoSidebar: boolean;
onModelPersistenceChange: () => void;
onSave: (
question: Question,
config?: { rerunQuery: boolean },
) => Promise<void>;
className?: string;
style?: CSSProperties;
};
export function ViewTitleHeader({
cancelQuery,
className,
isActionListVisible,
isAdditionalInfoVisible,
isBookmarked,
isDirty,
isNativeEditorOpen,
isNavBarOpen,
isObjectDetail,
isResultDirty,
isRunnable,
isRunning,
isShowingQuestionInfoSidebar,
isShowingSummarySidebar,
onCloseQuestionInfo,
onCloseSummary,
onEditSummary,
onModelPersistenceChange,
onOpenModal,
onOpenQuestionInfo,
onSave,
originalQuestion,
queryBuilderMode,
question,
result,
runQuestionQuery,
setQueryBuilderMode,
style,
toggleBookmark,
turnDatasetIntoQuestion,
updateQuestion,
}: ViewTitleHeaderProps) {
const [
areFiltersExpanded,
{ turnOn: expandFilters, turnOff: collapseFilters },
] = useToggle(!question?.isSaved());
const previousQuestion = usePrevious(question);
const query = question.query();
const previousQuery = usePrevious(query);
useEffect(() => {
const { isNative } = Lib.queryDisplayInfo(query);
const isPreviousQuestionNative =
previousQuery && Lib.queryDisplayInfo(previousQuery).isNative;
if (isNative || isPreviousQuestionNative) {
return;
}
const filtersCount = Lib.filters(query, -1).length;
const previousFiltersCount = previousQuery
? Lib.filters(previousQuery, -1).length
: 0;
if (filtersCount > previousFiltersCount) {
expandFilters();
}
}, [previousQuestion, question, expandFilters, previousQuery, query]);
const { isNative } = Lib.queryDisplayInfo(query);
const isSaved = question.isSaved();
const isModelOrMetric =
question.type() === "model" || question.type() === "metric";
const isSummarized = Lib.aggregations(query, -1).length > 0;
return (
<>
<ViewHeaderContainer
className={className}
style={style}
data-testid="qb-header"
isNavBarOpen={isNavBarOpen}
>
<DashboardBackButton />
{isSaved ? (
<SavedQuestionLeftSide
question={question}
isObjectDetail={isObjectDetail}
isAdditionalInfoVisible={isAdditionalInfoVisible}
onOpenQuestionInfo={onOpenQuestionInfo}
onSave={onSave}
/>
) : (
<AdHocQuestionLeftSide
isNative={isNative}
isSummarized={isSummarized}
question={question}
originalQuestion={originalQuestion}
onOpenModal={onOpenModal}
isObjectDetail={isObjectDetail}
/>
)}
<ViewTitleHeaderRightSide
question={question}
result={result}
queryBuilderMode={queryBuilderMode}
isBookmarked={isBookmarked}
isRunnable={isRunnable}
isRunning={isRunning}
isNativeEditorOpen={isNativeEditorOpen}
isShowingSummarySidebar={isShowingSummarySidebar}
isDirty={isDirty}
isResultDirty={isResultDirty}
isActionListVisible={isActionListVisible}
runQuestionQuery={runQuestionQuery}
cancelQuery={cancelQuery}
onOpenModal={onOpenModal}
onEditSummary={onEditSummary}
onCloseSummary={onCloseSummary}
setQueryBuilderMode={setQueryBuilderMode}
turnDatasetIntoQuestion={turnDatasetIntoQuestion}
toggleBookmark={toggleBookmark}
onOpenQuestionInfo={onOpenQuestionInfo}
onCloseQuestionInfo={onCloseQuestionInfo}
isShowingQuestionInfoSidebar={isShowingQuestionInfoSidebar}
onModelPersistenceChange={onModelPersistenceChange}
isObjectDetail={isObjectDetail}
isSaved={isSaved}
isModelOrMetric={isModelOrMetric}
areFiltersExpanded={areFiltersExpanded}
onExpandFilters={expandFilters}
onCollapseFilters={collapseFilters}
/>
</ViewHeaderContainer>
{FilterHeader.shouldRender({
question,
queryBuilderMode,
isObjectDetail,
}) && (
<FilterHeader
expanded={areFiltersExpanded}
question={question}
updateQuestion={updateQuestion}
/>
)}
</>
);
}

View File

@@ -1,6 +1,7 @@
import { t } from "ttag";
import { color } from "metabase/lib/colors";
import type { ModalType } from "metabase/query_builder/components/QueryModals";
import { MODAL_TYPES } from "metabase/query_builder/constants";
import * as Lib from "metabase-lib";
import type Question from "metabase-lib/v1/Question";
@@ -10,7 +11,7 @@ import { HeaderButton } from "../ViewHeader.styled";
interface FilterHeaderButtonProps {
className?: string;
onOpenModal: (modalType: string) => void;
onOpenModal: (modalType: ModalType) => void;
}
export function FilterHeaderButton({

View File

@@ -19,6 +19,7 @@ import {
} from "metabase/plugins";
import { softReloadCard } from "metabase/query_builder/actions";
import { trackTurnIntoModelClicked } from "metabase/query_builder/analytics";
import type { ModalType } from "metabase/query_builder/components/QueryModals";
import { MODAL_TYPES } from "metabase/query_builder/constants";
import { uploadFile } from "metabase/redux/uploads";
import { getUserIsAdmin } from "metabase/selectors/user";
@@ -29,6 +30,7 @@ import {
checkCanBeModel,
checkDatabaseCanPersistDatasets,
} from "metabase-lib/v1/metadata/utils/models";
import type { DatasetEditorTab, QueryBuilderMode } from "metabase-types/store";
import { UploadMode } from "metabase-types/store/upload";
import { ViewHeaderIconButtonContainer } from "../../ViewHeader.styled";
@@ -51,11 +53,14 @@ interface Props {
isBookmarked: boolean;
isShowingQuestionInfoSidebar: boolean;
handleBookmark: () => void;
onOpenModal: (modalType: string) => void;
onOpenModal: (modalType: ModalType) => void;
question: Question;
setQueryBuilderMode: (
mode: string,
opt: { datasetEditorTab: string },
mode: QueryBuilderMode,
opts?: {
shouldUpdateUrl?: boolean;
datasetEditorTab?: DatasetEditorTab;
},
) => void;
turnDatasetIntoQuestion: () => void;
onInfoClick: () => void;

View File

@@ -1,3 +1,4 @@
import type { UpdateQuestionOpts } from "metabase/query_builder/actions";
import { FilterPanel, FilterPanelButton } from "metabase/querying";
import * as Lib from "metabase-lib";
import type Question from "metabase-lib/v1/Question";
@@ -33,7 +34,7 @@ export function FilterHeaderToggle({
interface FilterHeaderProps {
question: Question;
expanded: boolean;
updateQuestion: (question: Question, opts: { run: boolean }) => void;
updateQuestion: (question: Question, config?: UpdateQuestionOpts) => void;
}
export function FilterHeader({

View File

@@ -1,42 +0,0 @@
/* eslint-disable react/prop-types */
import { t } from "ttag";
import Tooltip from "metabase/core/components/Tooltip";
import * as Lib from "metabase-lib";
import { ButtonRoot } from "./QuestionNotebookButton.styled";
export function QuestionNotebookButton({
className,
question,
isShowingNotebook,
setQueryBuilderMode,
...props
}) {
return (
<Tooltip
tooltip={isShowingNotebook ? t`Hide editor` : t`Show editor`}
placement="top"
>
<ButtonRoot
borderless={!isShowingNotebook}
primary={isShowingNotebook}
medium
isSelected={isShowingNotebook}
className={className}
icon="notebook"
onClick={() =>
setQueryBuilderMode(isShowingNotebook ? "view" : "notebook")
}
{...props}
/>
</Tooltip>
);
}
QuestionNotebookButton.shouldRender = ({ question, isActionListVisible }) => {
const { isEditable, isNative } = Lib.queryDisplayInfo(question.query());
return (
!isNative && isEditable && isActionListVisible && !question.isArchived()
);
};

View File

@@ -1,16 +0,0 @@
import styled from "@emotion/styled";
import Button from "metabase/core/components/Button";
import { color } from "metabase/lib/colors";
interface ButtonRootProps {
isSelected?: boolean;
}
export const ButtonRoot = styled(Button)<ButtonRootProps>`
color: ${props => !props.isSelected && color("text-dark")};
&:hover {
color: ${props => !props.isSelected && color("brand")};
}
`;

View File

@@ -0,0 +1,54 @@
import { t } from "ttag";
import { Tooltip, Icon, ActionIcon } from "metabase/ui";
import * as Lib from "metabase-lib";
import type Question from "metabase-lib/v1/Question";
import type { DatasetEditorTab, QueryBuilderMode } from "metabase-types/store";
type QuestionNotebookButtonProps = {
isShowingNotebook: boolean;
setQueryBuilderMode: (
mode: QueryBuilderMode,
opts?: {
shouldUpdateUrl?: boolean;
datasetEditorTab?: DatasetEditorTab;
},
) => void;
};
export function QuestionNotebookButton({
isShowingNotebook,
setQueryBuilderMode,
}: QuestionNotebookButtonProps) {
return (
<Tooltip
label={isShowingNotebook ? t`Hide editor` : t`Show editor`}
data-placement="top"
position="top"
>
<ActionIcon
color="brand"
size="2rem"
variant={isShowingNotebook ? "filled" : "viewHeader"}
onClick={() =>
setQueryBuilderMode(isShowingNotebook ? "view" : "notebook")
}
>
<Icon size={14} name="notebook" />
</ActionIcon>
</Tooltip>
);
}
QuestionNotebookButton.shouldRender = ({
question,
isActionListVisible,
}: {
question: Question;
isActionListVisible: boolean;
}) => {
const { isEditable, isNative } = Lib.queryDisplayInfo(question.query());
return (
!isNative && isEditable && isActionListVisible && !question.isArchived()
);
};

View File

@@ -21,7 +21,6 @@ SavedQuestionLeftSide.propTypes = {
question: PropTypes.object.isRequired,
isObjectDetail: PropTypes.bool,
isAdditionalInfoVisible: PropTypes.bool,
isShowingQuestionDetailsSidebar: PropTypes.bool,
onOpenQuestionInfo: PropTypes.func.isRequired,
onSave: PropTypes.func,
};

View File

@@ -1,11 +1,11 @@
import cx from "classnames";
import PropTypes from "prop-types";
import { useCallback } from "react";
import { t } from "ttag";
import CS from "metabase/css/core/index.css";
import { SERVER_ERROR_TYPES } from "metabase/lib/errors";
import MetabaseSettings from "metabase/lib/settings";
import type { ModalType } from "metabase/query_builder/components/QueryModals";
import {
SaveButton,
ViewHeaderActionPanel,
@@ -22,73 +22,85 @@ import {
ToggleNativeQueryPreview,
} from "metabase/query_builder/components/view/ViewHeader/components";
import { canExploreResults } from "metabase/query_builder/components/view/ViewHeader/utils";
import { Tooltip } from "metabase/ui";
import * as Lib from "metabase-lib";
import type Question from "metabase-lib/v1/Question";
import type { Dataset } from "metabase-types/api";
import type { DatasetEditorTab, QueryBuilderMode } from "metabase-types/store";
ViewTitleHeaderRightSide.propTypes = {
question: PropTypes.object.isRequired,
result: PropTypes.object,
queryBuilderMode: PropTypes.oneOf(["view", "notebook"]),
isModelOrMetric: PropTypes.bool,
isSaved: PropTypes.bool,
isNative: PropTypes.bool,
isRunnable: PropTypes.bool,
isRunning: PropTypes.bool,
isNativeEditorOpen: PropTypes.bool,
isShowingSummarySidebar: PropTypes.bool,
isDirty: PropTypes.bool,
isResultDirty: PropTypes.bool,
isActionListVisible: PropTypes.bool,
runQuestionQuery: PropTypes.func,
updateQuestion: PropTypes.func.isRequired,
cancelQuery: PropTypes.func,
onOpenModal: PropTypes.func,
onEditSummary: PropTypes.func,
onCloseSummary: PropTypes.func,
setQueryBuilderMode: PropTypes.func,
turnDatasetIntoQuestion: PropTypes.func,
areFiltersExpanded: PropTypes.bool,
onExpandFilters: PropTypes.func,
onCollapseFilters: PropTypes.func,
isBookmarked: PropTypes.bool,
toggleBookmark: PropTypes.func,
onOpenQuestionInfo: PropTypes.func,
onCloseQuestionInfo: PropTypes.func,
isShowingQuestionInfoSidebar: PropTypes.bool,
onModelPersistenceChange: PropTypes.func,
onQueryChange: PropTypes.func,
type ViewTitleHeaderRightSideProps = {
question: Question;
result: Dataset | null;
queryBuilderMode: QueryBuilderMode;
isBookmarked: boolean;
isModelOrMetric: boolean;
isSaved: boolean;
isRunnable: boolean;
isRunning: boolean;
isNativeEditorOpen: boolean;
isShowingSummarySidebar: boolean;
isDirty: boolean;
isResultDirty: boolean;
isActionListVisible: boolean;
runQuestionQuery: (opts?: {
overrideWithQuestion?: Question;
shouldUpdateUrl?: boolean;
ignoreCache?: boolean;
}) => void;
cancelQuery: () => void;
onOpenModal: (modalType: ModalType) => void;
onEditSummary: () => void;
onCloseSummary: () => void;
setQueryBuilderMode: (
mode: QueryBuilderMode,
opts?: {
shouldUpdateUrl?: boolean;
datasetEditorTab?: DatasetEditorTab;
},
) => void;
turnDatasetIntoQuestion: () => void;
areFiltersExpanded: boolean;
onExpandFilters: () => void;
onCollapseFilters: () => void;
toggleBookmark: () => void;
onOpenQuestionInfo: () => void;
onCloseQuestionInfo: () => void;
isShowingQuestionInfoSidebar: boolean;
onModelPersistenceChange: () => void;
isObjectDetail: boolean;
};
export function ViewTitleHeaderRightSide(props) {
const {
question,
result,
queryBuilderMode,
isBookmarked,
toggleBookmark,
isSaved,
isModelOrMetric,
isRunnable,
isRunning,
isNativeEditorOpen,
isShowingSummarySidebar,
isDirty,
isResultDirty,
isActionListVisible,
runQuestionQuery,
cancelQuery,
onOpenModal,
onEditSummary,
onCloseSummary,
setQueryBuilderMode,
turnDatasetIntoQuestion,
areFiltersExpanded,
onExpandFilters,
onCollapseFilters,
isShowingQuestionInfoSidebar,
onCloseQuestionInfo,
onOpenQuestionInfo,
onModelPersistenceChange,
} = props;
export function ViewTitleHeaderRightSide({
question,
result,
queryBuilderMode,
isBookmarked,
isModelOrMetric,
isSaved,
isRunnable,
isRunning,
isNativeEditorOpen,
isShowingSummarySidebar,
isDirty,
isResultDirty,
isActionListVisible,
runQuestionQuery,
cancelQuery,
onOpenModal,
onEditSummary,
onCloseSummary,
setQueryBuilderMode,
turnDatasetIntoQuestion,
areFiltersExpanded,
onExpandFilters,
onCollapseFilters,
toggleBookmark,
onOpenQuestionInfo,
onCloseQuestionInfo,
isShowingQuestionInfoSidebar,
onModelPersistenceChange,
isObjectDetail,
}: ViewTitleHeaderRightSideProps) {
const isShowingNotebook = queryBuilderMode === "notebook";
const { isEditable } = Lib.queryDisplayInfo(question.query());
@@ -101,7 +113,7 @@ export function ViewTitleHeaderRightSide(props) {
// the `type` field is set to "question".
const hasSaveButton =
!isModelOrMetric &&
!!isDirty &&
isDirty &&
!question.isArchived() &&
isActionListVisible;
const isMissingPermissions =
@@ -128,7 +140,11 @@ export function ViewTitleHeaderRightSide(props) {
return (
<ViewHeaderActionPanel data-testid="qb-header-action-panel">
{FilterHeaderToggle.shouldRender(props) && (
{FilterHeaderToggle.shouldRender({
question,
queryBuilderMode,
isObjectDetail,
}) && (
<FilterHeaderToggle
className={cx(CS.ml2, CS.mr1)}
query={question.query()}
@@ -137,13 +153,23 @@ export function ViewTitleHeaderRightSide(props) {
onCollapse={onCollapseFilters}
/>
)}
{FilterHeaderButton.shouldRender(props) && (
{FilterHeaderButton.shouldRender({
question,
queryBuilderMode,
isObjectDetail,
isActionListVisible,
}) && (
<FilterHeaderButton
className={cx(CS.hide, CS.smShow)}
onOpenModal={onOpenModal}
/>
)}
{QuestionSummarizeWidget.shouldRender(props) && (
{QuestionSummarizeWidget.shouldRender({
question,
queryBuilderMode,
isObjectDetail,
isActionListVisible,
}) && (
<QuestionSummarizeWidget
className={cx(CS.hide, CS.smShow)}
isShowingSummarySidebar={isShowingSummarySidebar}
@@ -151,19 +177,19 @@ export function ViewTitleHeaderRightSide(props) {
onCloseSummary={onCloseSummary}
/>
)}
{QuestionNotebookButton.shouldRender(props) && (
<ViewHeaderIconButtonContainer>
<QuestionNotebookButton
iconSize={16}
question={question}
isShowingNotebook={isShowingNotebook}
setQueryBuilderMode={setQueryBuilderMode}
/>
</ViewHeaderIconButtonContainer>
)}
{ToggleNativeQueryPreview.shouldRender(props) && (
<ToggleNativeQueryPreview question={question} />
{QuestionNotebookButton.shouldRender({
question,
isActionListVisible,
}) && (
<QuestionNotebookButton
isShowingNotebook={isShowingNotebook}
setQueryBuilderMode={setQueryBuilderMode}
/>
)}
{ToggleNativeQueryPreview.shouldRender({
question,
queryBuilderMode,
}) && <ToggleNativeQueryPreview question={question} />}
{hasExploreResultsLink && <ExploreResultsLink question={question} />}
{hasRunButton && !isShowingNotebook && (
<ViewHeaderIconButtonContainer>
@@ -195,24 +221,30 @@ export function ViewTitleHeaderRightSide(props) {
/>
)}
{hasSaveButton && (
<SaveButton
role="button"
disabled={isSaveDisabled}
tooltip={{
tooltip: disabledSaveTooltip,
isEnabled: isSaveDisabled,
placement: "left",
}}
onClick={() => onOpenModal("save")}
>
{t`Save`}
</SaveButton>
<Tooltip label={disabledSaveTooltip} disabled={canSave} position="left">
<SaveButton
data-testid="qb-save-button"
px="md"
py="sm"
variant="subtle"
aria-disabled={isSaveDisabled || undefined}
data-disabled={isSaveDisabled || undefined}
onClick={event => {
event.preventDefault();
if (!isSaveDisabled) {
onOpenModal("save");
}
}}
>
{t`Save`}
</SaveButton>
</Tooltip>
)}
</ViewHeaderActionPanel>
);
}
function getDisabledSaveTooltip(isEditable) {
function getDisabledSaveTooltip(isEditable: boolean) {
if (!isEditable) {
return t`You don't have permission to save this question.`;
}

View File

@@ -13,6 +13,32 @@ export const getActionIconOverrides =
},
},
}),
filled: (theme, params) => ({
root: {
color: theme.fn.themeColor("white"),
backgroundColor: theme.fn.themeColor(params.color),
border: `1px solid ${theme.fn.themeColor(params.color)}`,
transition: "background 300ms linear, border 300ms linear",
"&:hover": {
backgroundColor: theme.fn.themeColor("white"),
border: `1px solid ${theme.fn.themeColor(params.color)}`,
color: theme.fn.themeColor(params.color),
},
},
}),
viewHeader: theme => ({
root: {
color: theme.fn.themeColor("text-dark"),
backgroundColor: "transparent",
border: "1px solid transparent",
transition: "background 300ms linear, border 300ms linear",
"&:hover": {
color: theme.fn.themeColor("brand"),
backgroundColor: theme.fn.themeColor("bg-medium"),
border: "1px solid transparent",
},
},
}),
},
},
});

View File

@@ -1 +1,3 @@
export { getActionIconOverrides } from "./ActionIcon.styled";
export { ActionIcon } from "@mantine/core";
export type { ActionIconProps } from "@mantine/core";

View File

@@ -134,7 +134,7 @@ export const getButtonOverrides = (): MantineThemeOverride["components"] => ({
color: hoverColor,
backgroundColor: "transparent",
},
"&:disabled": {
"&:disabled, &[data-disabled=true]": {
color: theme.fn.themeColor("text-light"),
borderColor: "transparent",
backgroundColor: "transparent",