Skip to content
Snippets Groups Projects
Unverified Commit 3287965d authored by Jesse Devaney's avatar Jesse Devaney Committed by GitHub
Browse files

New Dashboard Primitive - Headings (#30804)

* initial prototype commit

* add default value for text cards

* auto focus text/heading cards on creation

* remove default value - use placeholders instead

* fix inability to edit when there is no heading value

* remove unnecessary comment

* re-word heading & text tooltip

* refactor and fix draggable area bug

* fix border artifacts when combined with dragging styling

* fix stroke width for hovering and focused - make them the same

* display placeholder value in editing mode if content is empty for text and heading dash-cards

* remove duplicate placeholders

* allow pointer-events anywhere on the div (not just content)

* refactor styling to apply no content borders

* Fix heading overflow

* refactor text dash-card button

- remove unnecessary redux store usage

* refactor border styling to one css format

* rename class names to match syntax conventions

* refactor auto-preview styling for dash-card root

* refactor style name

* remove comment

* undo unnecessary re-formatting

* refactor popover styling

* remove comment that was previously addressed

* refactor to typescript

* remove comment

* remove un-used code

* fix TypeScript errors in text unit spec

* update button key and event description

* add e2e tests for text and heading dash-cards

* add e2e tests

* add small heading unit tests

* fix typescript error

* add aria-label removed by merging with master

* fix broken test

* fix broken tests

* fix typescript errors

* align edit text with preview text

* change from hover editing to click editing

* update tests for click editing

- remove hover tests
- change focus tests to click tests
- remove top-level findByText() calls

* refactor to use-focus hook

* add "heading" to getSupportedCardsForSubscriptions

* rename showVisualizationOptions to showDashcardVisualizationSettings

* add aria label to buttons and query in tests by label

* refactor E2E tests to make longer flows

* add existing metabase types to Heading

* refactor heading and text menu to use EntityMenu

* fix aria-label

* [skip ci] fix css syntax error from missing semi-colon

* adjust styles and use rem value standards

* [skip ci] prevent drag on click to edit surface area

* tweak styles for mobile resolutions

* fix react import lint error

* fix E2E tests

* refactor e2e helper to shorthand

* refactor to use e2e helper

* refactor to use saveDashboard() e2e helper

* refactor e2e tests

* rename styling variables

* null coalesce minWidth

* remove unnecessary anonymous function wrapping

* refactor to useToggle

* improve no content check

* use object creation shorthand

* simplify expression

* refactor empty content styling

* move text-edit-container styling into heading and text components

* refactor resize handle styling and abstract condition into function

* refactor to use CSS function for string styling

* improve Heading unit tests

* improve Text unit tests

* update Heading and Text unit tests

* update for new Icons

* fix E2E tests

* refactor E2E test assertions

* refactor click events to use user-event library

* fix tooltip positioning
parent d95d69df
No related branches found
No related tags found
No related merge requests found
Showing
with 608 additions and 218 deletions
......@@ -64,8 +64,16 @@ export function showDashboardCardActions(index = 0) {
getDashboardCard(index).realHover();
}
export function showDashcardVisualizationSettings(index = 0) {
return getDashboardCard(index)
.realHover()
.within(() => {
cy.findByLabelText("Show visualization options").click();
});
}
export function editDashboard() {
cy.icon("pencil").click();
cy.findByLabelText("Edit dashboard").click();
cy.findByText("You're editing this dashboard.");
}
......@@ -98,14 +106,34 @@ export function setFilter(type, subType) {
});
}
export function createEmptyTextBox() {
cy.findByLabelText("Edit dashboard").click();
cy.findByLabelText("Add a heading or text box").click();
popover().findByText("Text").click();
}
export function addTextBox(string, options = {}) {
cy.icon("pencil").click();
cy.icon("string").click();
cy.findByLabelText("Edit dashboard").click();
cy.findByLabelText("Add a heading or text box").click();
popover().findByText("Text").click();
cy.findByPlaceholderText(
"You can use Markdown here, and include variables {{like_this}}",
).type(string, options);
}
export function createEmptyHeading() {
cy.findByLabelText("Edit dashboard").click();
cy.findByLabelText("Add a heading or text box").click();
popover().findByText("Heading").click();
}
export function addHeading(string, options = {}) {
cy.findByLabelText("Edit dashboard").click();
cy.findByLabelText("Add a heading or text box").click();
popover().findByText("Heading").click();
cy.findByPlaceholderText("Heading").type(string, options);
}
export function openQuestionsSidebar() {
cy.findByLabelText("Add questions").click();
}
......
import {
restore,
showDashboardCardActions,
popover,
visitDashboard,
addTextBox,
} from "e2e/support/helpers";
describe("scenarios > dashboard > text-box", () => {
beforeEach(() => {
restore();
cy.signInAsAdmin();
});
describe("Editing", () => {
beforeEach(() => {
// Create text box card
visitDashboard(1);
addTextBox("Text *text* __text__");
});
it("should render correct icons for preview and edit modes", () => {
showDashboardCardActions(1);
// edit mode
cy.icon("eye").click();
// preview mode
cy.icon("edit_document");
});
it("should render visualization options (metabase#22061)", () => {
showDashboardCardActions(1);
// edit mode
cy.icon("palette").eq(1).click();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Vertical Alignment");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Horizontal Alignment");
});
it("should not render edit and preview actions when not editing", () => {
// Exit edit mode and check for edit options
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Save").click();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("You are editing a dashboard").should("not.exist");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.contains("Text text text");
cy.icon("edit_document").should("not.exist");
cy.icon("eye").should("not.exist");
});
it("should switch between rendered markdown and textarea input", () => {
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Text *text* __text__");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Save").click();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.contains("Text text text");
});
});
describe("when text-box is the only element on the dashboard", () => {
beforeEach(() => {
cy.createDashboard().then(({ body: { id } }) => {
cy.intercept("PUT", `/api/dashboard/${id}`).as("dashboardUpdated");
visitDashboard(id);
});
});
// fixed in metabase#11358
it("should load after save/refresh (metabase#12873)", () => {
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Test Dashboard");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("This dashboard is looking empty.");
// Add save text box to dash
addTextBox("Dashboard testing text");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Save").click();
// Reload page
cy.reload();
// Page should still load
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("New");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Loading...").should("not.exist");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Cannot read property 'type' of undefined").should(
"not.exist",
);
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Test Dashboard");
// Text box should still load
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Dashboard testing text");
});
it("should have a scroll bar for long text (metabase#8333)", () => {
addTextBox(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam ut fermentum erat, nec sagittis justo. Vivamus vitae ipsum semper, consectetur odio at, rutrum nisi. Fusce maximus consequat porta. Mauris libero mi, viverra ac hendrerit quis, rhoncus quis ante. Pellentesque molestie ut felis non congue. Vivamus finibus ligula id fringilla rutrum. Donec quis dignissim ligula, vitae tempor urna.\n\nDonec quis enim porta, porta lacus vel, maximus lacus. Sed iaculis leo tortor, vel tempor velit tempus vitae. Nulla facilisi. Vivamus quis sagittis magna. Aenean eu eros augue. Sed euismod pulvinar laoreet. Morbi commodo, sem sed dictum faucibus, sem ante ultrices libero, nec ornare risus lacus eget velit. Etiam sagittis lectus non erat tristique tempor. Sed in ipsum urna. Sed venenatis turpis at orci feugiat, ut gravida lectus luctus.",
);
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Save").click();
cy.wait("@dashboardUpdated");
// The test fails if there is no scroll bar
cy.get(".text-card-markdown")
.should("have.css", "overflow-x", "hidden")
.should("have.css", "overflow-y", "auto")
.scrollTo("bottom");
});
it("should render html links, and not just the markdown flavor of them (metabase#18114)", () => {
addTextBox(
"- Visit https://www.metabase.com{enter}- Or go to [Metabase](https://www.metabase.com)",
);
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Save").click();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("You're editing this dashboard.").should("not.exist");
cy.get(".Card")
.findAllByRole("link")
.should("be.visible")
.and("have.length", 2);
});
});
it("should let you add a parameter to a dashboard with a text box (metabase#11927)", () => {
visitDashboard(1);
// click pencil icon to edit
cy.icon("pencil").click();
// add text box with text
cy.icon("string").click();
cy.get(".DashCard").last().find("textarea").type("text text text");
cy.icon("filter").click();
popover().within(() => {
cy.findByText("Text or Category").click();
cy.findByText("Is").click();
});
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Save").click();
// confirm text box and filter are still there
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("text text text");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Text");
});
});
import {
restore,
getDashboardCard,
popover,
visitDashboard,
addTextBox,
editDashboard,
saveDashboard,
} from "e2e/support/helpers";
describe("scenarios > dashboard > text and headings", () => {
beforeEach(() => {
restore();
cy.signInAsAdmin();
});
describe("text", () => {
beforeEach(() => {
visitDashboard(1);
});
it("should allow creation, editing, and saving of text boxes", () => {
// should be able to create new text box
editDashboard();
cy.findByLabelText("Add a heading or text box").click();
popover().findByText("Text").click();
getDashboardCard(1).within(() => {
// textarea should:
// 1. be auto-focused on creation
// 2. have no value
// 3. have placeholder "You can use Markdown here, and include variables {{like_this}}"
cy.get("textarea")
.should("have.focus")
.should("have.value", "")
.should(
"have.attr",
"placeholder",
"You can use Markdown here, and include variables {{like_this}}",
);
});
// should auto-preview on blur (de-focus)
cy.findByTestId("edit-bar")
.findByText("You're editing this dashboard.")
.click(); // un-focus text
getDashboardCard(1).within(() => {
// preview should have no textarea element
cy.get("textarea").should("not.exist");
// if no content has been entered, preview should have placeholder content
cy.findByText(
"You can use Markdown here, and include variables {{like_this}}",
).should("be.visible");
});
// should focus textarea editor on click
getDashboardCard(1)
.click()
.within(() => {
cy.get("textarea").should("have.focus");
});
// should be able to edit text while focused
cy.focused().type("Text *text* __text__");
// should auto-preview typed text
cy.findByTestId("edit-bar")
.findByText("You're editing this dashboard.")
.click(); // un-focus text
getDashboardCard(1).contains("Text text text").should("be.visible");
// should render visualization options
getDashboardCard(1)
.realHover()
.within(() => {
cy.findByLabelText("Show visualization options").click();
});
cy.findByRole("dialog").within(() => {
cy.findByTestId("chartsettings-sidebar").within(() => {
cy.findByText("Vertical Alignment").should("be.visible");
cy.findByText("Horizontal Alignment").should("be.visible");
cy.findByText("Show background").should("be.visible");
});
cy.findByText("Cancel").click(); // dismiss modal
});
// should not render edit and preview actions
getDashboardCard(1)
.realHover()
.within(() => {
cy.findByLabelText("Edit card").should("not.exist");
cy.findByLabelText("Preview card").should("not.exist");
});
// should allow saving and show up after refresh
saveDashboard();
getDashboardCard(1).contains("Text text text").should("be.visible");
});
it("should have a scroll bar for long text (metabase#8333)", () => {
addTextBox(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam ut fermentum erat, nec sagittis justo. Vivamus vitae ipsum semper, consectetur odio at, rutrum nisi. Fusce maximus consequat porta. Mauris libero mi, viverra ac hendrerit quis, rhoncus quis ante. Pellentesque molestie ut felis non congue. Vivamus finibus ligula id fringilla rutrum. Donec quis dignissim ligula, vitae tempor urna.\n\nDonec quis enim porta, porta lacus vel, maximus lacus. Sed iaculis leo tortor, vel tempor velit tempus vitae. Nulla facilisi. Vivamus quis sagittis magna. Aenean eu eros augue. Sed euismod pulvinar laoreet. Morbi commodo, sem sed dictum faucibus, sem ante ultrices libero, nec ornare risus lacus eget velit. Etiam sagittis lectus non erat tristique tempor. Sed in ipsum urna. Sed venenatis turpis at orci feugiat, ut gravida lectus luctus.",
{ delay: 1 },
);
cy.findByTestId("edit-bar").findByText("Save").click();
// The test fails if there is no scroll bar
getDashboardCard(1)
.get(".text-card-markdown")
.should("have.css", "overflow-x", "hidden")
.should("have.css", "overflow-y", "auto")
.scrollTo("bottom");
});
it("should let you add a parameter to a dashboard with a text box (metabase#11927)", () => {
addTextBox("text text text");
cy.findByLabelText("Add a filter").click();
popover().within(() => {
cy.findByText("Text or Category").click();
cy.findByText("Is").click();
});
cy.findByTestId("edit-bar").findByText("Save").click();
// confirm text box and filter are still there
getDashboardCard(1).contains("text text text").should("be.visible");
cy.findByTestId("dashboard-parameters-widget-container")
.findByText("Text")
.should("be.visible");
});
});
describe("heading", () => {
beforeEach(() => {
visitDashboard(1);
});
it("should allow creation, editing, and saving of heading component", () => {
// should be able to create new heading
editDashboard();
cy.findByLabelText("Add a heading or text box").click();
popover().findByText("Heading").click();
getDashboardCard(1).within(() => {
// heading input should
// 1. be auto-focused on creation
// 2. have no value
// 3. have placeholder "Heading"
cy.get("input")
.should("have.focus")
.should("have.value", "")
.should("have.attr", "placeholder", "Heading");
});
// should auto-preview on blur (de-focus)
cy.findByTestId("edit-bar")
.findByText("You're editing this dashboard.")
.click(); // un-focus heading
getDashboardCard(1).within(() => {
// preview mode should have no input
cy.get("input").should("not.exist");
// if no content has been entered, preview should have placeholder "Heading"
cy.get("h2").findByText("Heading").should("be.visible");
});
// should focus input editor on click
getDashboardCard(1)
.click()
.within(() => {
cy.get("input").should("have.focus");
});
// should be able to edit text while focused
cy.focused().type("Example Heading");
// should auto-preview typed text
cy.findByTestId("edit-bar")
.findByText("You're editing this dashboard.")
.click(); // un-focus heading
getDashboardCard(1)
.get("h2")
.findByText("Example Heading")
.should("be.visible");
// should have no visualization options
getDashboardCard(1)
.realHover()
.within(() => {
cy.findByLabelText("Show visualization options").should("not.exist");
});
// should not render edit and preview actions
getDashboardCard(1)
.realHover()
.within(() => {
cy.findByLabelText("Edit card").should("not.exist");
cy.findByLabelText("Preview card").should("not.exist");
});
// should allow saving and show up after refresh
saveDashboard();
getDashboardCard(1)
.get("h2")
.findByText("Example Heading")
.should("be.visible");
});
});
});
......@@ -25,7 +25,6 @@ describe("scenarios > dashboard > parameters in text cards", () => {
addTextBox("Text card with no variables", {
parseSpecialCharSequences: false,
});
editDashboard();
setFilter("Number", "Equal to");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText(
......@@ -36,7 +35,6 @@ describe("scenarios > dashboard > parameters in text cards", () => {
it("should allow dashboard filters to be connected to tags in text cards", () => {
addTextBox("Variable: {{foo}}", { parseSpecialCharSequences: false });
editDashboard();
setFilter("Number", "Equal to");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
......@@ -77,7 +75,6 @@ describe("scenarios > dashboard > parameters in text cards", () => {
cy.reload();
addTextBox("Variable: {{foo}}", { parseSpecialCharSequences: false });
editDashboard();
setFilter("Time", "Relative Date");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
......@@ -137,7 +134,13 @@ describe("scenarios > dashboard > parameters in text cards", () => {
cy.findByText("Single Date").click();
// Create text card and connect parameter
addTextBox("Variable: {{foo}}", { parseSpecialCharSequences: false });
cy.findByLabelText("Add a heading or text box").click();
popover().within(() => {
cy.findByText("Text").click();
});
cy.findByPlaceholderText(
"You can use Markdown here, and include variables {{like_this}}",
).type("Variable: {{foo}}", { parseSpecialCharSequences: false });
cy.findByText("Single Date").click();
cy.findByText("Select…").click();
cy.findByText("foo").click();
......
......@@ -9,6 +9,7 @@ import {
visitDashboard,
sendEmailAndAssert,
addOrUpdateDashboardCard,
addTextBox,
} from "e2e/support/helpers";
import { USERS } from "e2e/support/cypress_data";
......@@ -41,13 +42,7 @@ describe("scenarios > dashboard > subscriptions", () => {
cy.createDashboard().then(({ body: { id: DASHBOARD_ID } }) => {
visitDashboard(DASHBOARD_ID);
});
cy.icon("pencil").click();
cy.icon("string").click();
cy.findByPlaceholderText(
"You can use Markdown here, and include variables {{like_this}}",
)
.click()
.type("Foo");
addTextBox("Foo");
cy.button("Save").click();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("You're editing this dashboard.").should("not.exist");
......@@ -247,11 +242,7 @@ describe("scenarios > dashboard > subscriptions", () => {
const TEXT_CARD = "FooBar";
visitDashboard(1);
cy.icon("pencil").click();
cy.icon("string").click();
cy.findByPlaceholderText(
"You can use Markdown here, and include variables {{like_this}}",
).type(TEXT_CARD);
addTextBox(TEXT_CARD);
cy.button("Save").click();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("You're editing this dashboard.").should("not.exist");
......
......@@ -54,6 +54,7 @@ class EntityMenu extends Component {
className,
openClassNames,
closedClassNames,
minWidth,
tooltip,
trigger,
renderTrigger,
......@@ -127,7 +128,7 @@ class EntityMenu extends Component {
>
<Card>
{menuItemContent || (
<ol className="p1" style={{ minWidth: 184 }}>
<ol className="p1" style={{ minWidth: minWidth ?? 184 }}>
{items.map(item => {
if (!item) {
return null;
......
......@@ -162,6 +162,10 @@
box-shadow: 3px 3px 8px var(--color-shadow);
}
.BrandColorResizeHandle .react-resizable-handle:after {
border-color: var(--color-brand) !important;
}
.Dash--editing .DashCard.react-draggable-dragging,
.Dash--editing .DashCard.react-resizable-resizing {
z-index: 3;
......
......@@ -99,15 +99,38 @@ export const addDashCardToDashboard = function ({
};
};
export const addTextDashCardToDashboard = function ({ dashId, tabId }) {
const virtualTextCard = createCard();
virtualTextCard.display = "text";
virtualTextCard.archived = false;
export const addMarkdownDashCardToDashboard = function ({ dashId, tabId }) {
const virtualTextCard = {
...createCard(),
display: "text",
archived: false,
};
const dashcardOverrides = {
card: virtualTextCard,
visualization_settings: {
virtual_card: virtualTextCard,
},
};
return addDashCardToDashboard({
dashId: dashId,
dashcardOverrides: dashcardOverrides,
tabId,
});
};
export const addHeadingDashCardToDashboard = function ({ dashId, tabId }) {
const virtualTextCard = {
...createCard(),
display: "heading",
archived: false,
};
const dashcardOverrides = {
card: virtualTextCard,
visualization_settings: {
virtual_card: virtualTextCard,
"dashcard.background": false,
},
};
return addDashCardToDashboard({
......
......@@ -6,6 +6,7 @@ export interface DashCardRootProps {
isNightMode: boolean;
isUsuallySlow: boolean;
hasHiddenBackground: boolean;
shouldForceHiddenBackground: boolean;
}
const rootNightModeStyle = css`
......@@ -23,6 +24,11 @@ const rootTransparentBackgroundStyle = css`
box-shadow: none !important;
`;
const hiddenBackgroundStyle = css`
background: ${color("bg-light")};
box-shadow: none !important;
`;
export const DashCardRoot = styled.div<DashCardRootProps>`
background-color: ${color("white")};
......@@ -30,6 +36,9 @@ export const DashCardRoot = styled.div<DashCardRootProps>`
${({ isUsuallySlow }) => isUsuallySlow && rootSlowCardStyle}
${({ hasHiddenBackground }) =>
hasHiddenBackground && rootTransparentBackgroundStyle}
${({ shouldForceHiddenBackground }) =>
shouldForceHiddenBackground && hiddenBackgroundStyle}
`;
export const DashboardCardActionsPanel = styled.div`
......
......@@ -190,6 +190,20 @@ function DashCard({
[dashcard],
);
const shouldForceHiddenBackground = useMemo(() => {
if (!isEditing) {
return false;
}
const isHeadingCard = mainCard.display === "heading";
const isTextCard = mainCard.display === "text";
return (
(isHeadingCard || isTextCard) &&
mainCard.visualization_settings["dashcard.background"] === false
);
}, [isEditing, mainCard]);
const hasHiddenBackground = useMemo(() => {
if (isEditing) {
return false;
......@@ -284,6 +298,7 @@ function DashCard({
data-testid="dashcard"
className="Card rounded flex flex-column hover-parent hover--visibility"
hasHiddenBackground={hasHiddenBackground}
shouldForceHiddenBackground={shouldForceHiddenBackground}
isNightMode={isNightMode}
isUsuallySlow={isSlow === "usually-slow"}
ref={cardRootRef}
......
......@@ -31,7 +31,10 @@ function ChartSettingsButton({
wide
tall
triggerElement={
<DashCardActionButton tooltip={t`Visualization options`}>
<DashCardActionButton
tooltip={t`Visualization options`}
aria-label={t`Show visualization options`}
>
<DashCardActionButton.Icon name="palette" />
</DashCardActionButton>
}
......
......@@ -70,6 +70,7 @@ function DashCardActionButtons({
<DashCardActionButton
onClick={onPreviewToggle}
tooltip={isPreviewing ? t`Edit` : t`Preview`}
aria-label={isPreviewing ? t`Edit card` : t`Preview card`}
analyticsEvent="Dashboard;Text;edit"
>
{isPreviewing ? (
......
......@@ -204,6 +204,7 @@ class DashboardGrid extends Component {
action: 1,
link: 1,
text: 2,
heading: 2,
scalar: 4,
},
});
......@@ -355,19 +356,30 @@ class DashboardGrid extends Component {
breakpoint,
gridItemWidth,
totalNumGridCols,
}) => (
<DashboardCard
key={String(dc.id)}
className="DashCard"
isAnimationDisabled={this.state.isAnimationPaused}
>
{this.renderDashCard(dc, {
isMobile: breakpoint === "mobile",
gridItemWidth,
totalNumGridCols,
})}
</DashboardCard>
);
}) => {
const { isEditing } = this.props;
const shouldChangeResizeHandle = isEditingTextOrHeadingCard(
dc.card.display,
isEditing,
);
return (
<DashboardCard
key={String(dc.id)}
className={cx("DashCard", {
BrandColorResizeHandle: shouldChangeResizeHandle,
})}
isAnimationDisabled={this.state.isAnimationPaused}
>
{this.renderDashCard(dc, {
isMobile: breakpoint === "mobile",
gridItemWidth,
totalNumGridCols,
})}
</DashboardCard>
);
};
renderGrid() {
const { width } = this.props;
......@@ -408,6 +420,12 @@ class DashboardGrid extends Component {
}
}
function isEditingTextOrHeadingCard(display, isEditing) {
const isTextOrHeadingCard = display === "heading" || display === "text";
return isEditing && isTextOrHeadingCard;
}
export default _.compose(
ExplicitSize(),
connect(null, mapDispatchToProps),
......
import styled from "@emotion/styled";
export const IconContainer = styled.div`
display: flex;
column-gap: 0.25rem;
align-items: center;
`;
import { t } from "ttag";
import { Icon } from "metabase/core/components/Icon";
import EntityMenu from "metabase/components/EntityMenu";
import { DashboardHeaderButton } from "metabase/dashboard/containers/DashboardHeader.styled";
import { IconContainer } from "./TextOptionsButton.styled";
interface TextOptionsButtonProps {
onAddMarkdown: () => void;
onAddHeading: () => void;
}
export function TextOptionsButton({
onAddMarkdown,
onAddHeading,
}: TextOptionsButtonProps) {
const TEXT_OPTIONS = [
{
title: t`Heading`,
action: onAddHeading,
event: "Dashboard; Add Heading",
},
{
title: t`Text`,
action: onAddMarkdown,
event: "Dashboard; Add Markdown Box",
},
];
return (
<EntityMenu
items={TEXT_OPTIONS}
trigger={
<DashboardHeaderButton aria-label={t`Add a heading or text box`}>
<IconContainer>
<Icon name="string" size={18} />
<Icon name="chevrondown" size={10} />
</IconContainer>
</DashboardHeaderButton>
}
minWidth={90}
/>
);
}
......@@ -18,6 +18,7 @@ import Bookmark from "metabase/entities/bookmarks";
import { getDashboardActions } from "metabase/dashboard/components/DashboardActions";
import { TextOptionsButton } from "metabase/dashboard/components/TextOptions/TextOptionsButton";
import ParametersPopover from "metabase/dashboard/components/ParametersPopover";
import DashboardBookmark from "metabase/dashboard/components/DashboardBookmark";
import TippyPopover from "metabase/components/Popover/TippyPopover";
......@@ -87,7 +88,8 @@ class DashboardHeader extends Component {
setRefreshElapsedHook: PropTypes.func.isRequired,
addCardToDashboard: PropTypes.func.isRequired,
addTextDashCardToDashboard: PropTypes.func.isRequired,
addHeadingDashCardToDashboard: PropTypes.func.isRequired,
addMarkdownDashCardToDashboard: PropTypes.func.isRequired,
addLinkDashCardToDashboard: PropTypes.func.isRequired,
fetchDashboard: PropTypes.func.isRequired,
saveDashboardAndCards: PropTypes.func.isRequired,
......@@ -99,7 +101,6 @@ class DashboardHeader extends Component {
onFullscreenChange: PropTypes.func.isRequired,
onSharingClick: PropTypes.func.isRequired,
onChangeLocation: PropTypes.func.isRequired,
toggleSidebar: PropTypes.func.isRequired,
......@@ -134,8 +135,15 @@ class DashboardHeader extends Component {
toggleBookmark(this.props.dashboardId);
}
onAddTextBox() {
this.props.addTextDashCardToDashboard({
onAddMarkdownBox() {
this.props.addMarkdownDashCardToDashboard({
dashId: this.props.dashboard.id,
tabId: this.props.selectedTabId,
});
}
onAddHeading() {
this.props.addHeadingDashCardToDashboard({
dashId: this.props.dashboard.id,
tabId: this.props.selectedTabId,
});
......@@ -270,21 +278,21 @@ class DashboardHeader extends Component {
</Tooltip>,
);
// Add text card button
// Text/Headers
buttons.push(
<Tooltip key="add-a-text-box" tooltip={t`Add a text box`}>
<a
data-metabase-event="Dashboard;Add Text Box"
key="add-text"
aria-label={t`Add a text box`}
className="text-brand-hover cursor-pointer"
onClick={() => this.onAddTextBox()}
>
<DashboardHeaderButton>
<Icon name="string" size={18} />
</DashboardHeaderButton>
</a>
<Tooltip
key="dashboard-add-heading-or-text-button"
tooltip={t`Add a heading or text`}
>
<TextOptionsButton
onAddMarkdown={() => this.onAddMarkdownBox()}
onAddHeading={() => this.onAddHeading()}
/>
</Tooltip>,
);
// Add link card button
buttons.push(
<Tooltip key="add-link-card" tooltip={t`Add link card`}>
<DashboardHeaderButton
onClick={() => this.onAddLinkCard()}
......@@ -321,6 +329,7 @@ class DashboardHeader extends Component {
<DashboardHeaderButton
key="parameters"
onClick={showAddParameterPopover}
aria-label={t`Add a filter`}
>
<Icon name="filter" />
</DashboardHeaderButton>
......
......@@ -59,7 +59,7 @@ const cardsFromDashboard = dashboard => {
const getSupportedCardsForSubscriptions = dashboard => {
return cardsFromDashboard(dashboard).filter(
card => !["text", "action", "link"].includes(card.display),
card => !["text", "heading", "action", "link"].includes(card.display),
);
};
......
......@@ -9,7 +9,7 @@ import Scalar from "./visualizations/Scalar";
import SmartScalar from "./visualizations/SmartScalar";
import Progress from "./visualizations/Progress";
import Table from "./visualizations/Table";
import Text from "./visualizations/Text";
import { Text } from "./visualizations/Text";
import LinkViz from "./visualizations/LinkViz";
import LineChart from "./visualizations/LineChart";
import BarChart from "./visualizations/BarChart";
......@@ -24,6 +24,7 @@ import Funnel from "./visualizations/Funnel";
import Gauge from "./visualizations/Gauge";
import ObjectDetail from "./visualizations/ObjectDetail";
import PivotTable from "./visualizations/PivotTable";
import { Heading } from "./visualizations/Heading";
export default function () {
registerVisualization(Scalar);
......@@ -46,5 +47,6 @@ export default function () {
registerVisualization(ObjectDetail);
registerVisualization(PivotTable);
registerVisualization(ActionViz);
registerVisualization(Heading);
setDefaultVisualization(Table);
}
import { css } from "@emotion/react";
import styled from "@emotion/styled";
import { color } from "metabase/lib/colors";
interface InputContainerProps {
isPreviewing: boolean;
isEmpty: boolean;
}
export const InputContainer = styled.div<InputContainerProps>`
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
justify-content: center;
overflow: hidden;
padding-left: 0.75rem;
pointer-events: auto;
border-radius: 8px;
&:hover {
padding-left: calc(0.75rem - 1px); // adjust for border on hover
}
.DashCard:hover &,
.DashCard:focus-within & {
border: 1px solid ${color("brand")};
}
.DashCard.resizing & {
border: 1px solid ${color("brand")};
}
${({ isPreviewing, isEmpty }) =>
(!isPreviewing || isEmpty) &&
css`
padding-left: calc(0.75rem - 1px);
`} // adjust for border on preview/no entered content
${({ isEmpty }) =>
isEmpty &&
css`
border: 1px solid ${color("brand")};
color: ${color("text-light")};
`}
`;
export const TextInput = styled.input`
border: none;
background: none;
max-height: 50%;
color: ${color("text-dark")};
font-size: 1.375rem;
font-weight: 700;
height: inherit;
min-height: unset;
outline: none;
padding: 0.25rem 0;
pointer-events: all;
resize: none;
width: 100%;
`;
export const HeadingContainer = styled.div`
display: flex;
flex-direction: column;
height: 100%;
justify-content: center;
overflow: hidden;
padding-left: 0.75rem;
width: 100%;
`;
interface HeadingContentProps {
isEditing?: boolean;
}
export const HeadingContent = styled.h2<HeadingContentProps>`
max-height: 100%;
max-width: 100%;
overflow-x: hidden;
overflow-y: auto;
font-size: 1.375rem;
padding: 0;
margin: 0;
pointer-events: all;
${({ isEditing }) =>
isEditing &&
css`
cursor: text;
`}
`;
import { useMemo, MouseEvent } from "react";
import { t } from "ttag";
import { useToggle } from "metabase/hooks/use-toggle";
import { isEmpty } from "metabase/lib/validate";
import type {
BaseDashboardOrderedCard,
VisualizationSettings,
} from "metabase-types/api";
import {
InputContainer,
HeadingContent,
HeadingContainer,
TextInput,
} from "./Heading.styled";
interface HeadingProps {
isEditing: boolean;
onUpdateVisualizationSettings: ({ text }: { text: string }) => void;
dashcard: BaseDashboardOrderedCard;
settings: VisualizationSettings;
}
export function Heading({
settings,
isEditing,
onUpdateVisualizationSettings,
dashcard,
}: HeadingProps) {
const justAdded = useMemo(() => dashcard?.justAdded || false, [dashcard]);
const [isFocused, { turnOn: toggleFocusOn, turnOff: toggleFocusOff }] =
useToggle(justAdded);
const isPreviewing = !isFocused;
const handleTextChange = (text: string) =>
onUpdateVisualizationSettings({ text });
const preventDragging = (e: MouseEvent<HTMLInputElement>) =>
e.stopPropagation();
const content = settings.text;
const hasContent = !isEmpty(content);
const placeholder = t`Heading`;
if (isEditing) {
return (
<InputContainer
data-testid="editing-dashboard-heading-container"
isEmpty={!hasContent}
isPreviewing={isPreviewing}
onClick={toggleFocusOn}
>
{isPreviewing ? (
<HeadingContent
data-testid="editing-dashboard-heading-preview"
isEditing={isEditing}
onMouseDown={preventDragging}
>
{hasContent ? content : placeholder}
</HeadingContent>
) : (
<TextInput
name="heading"
data-testid="editing-dashboard-heading-input"
placeholder={placeholder}
value={content}
autoFocus={justAdded || isFocused}
onChange={e => handleTextChange(e.target.value)}
onMouseDown={preventDragging}
onBlur={toggleFocusOff}
/>
)}
</InputContainer>
);
}
return (
<HeadingContainer>
<HeadingContent data-testid="saved-dashboard-heading-content">
{content}
</HeadingContent>
</HeadingContainer>
);
}
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