Skip to content
Snippets Groups Projects
Unverified Commit 9ce72a44 authored by Anton Kulyk's avatar Anton Kulyk Committed by GitHub
Browse files

Bump dashboard description max length limit to 1.5K (#44470)

* Bump dashboard description max len limit to 1.5K

* Extract `DASHBOARD_DESCRIPTION_MAX_LENGTH` const

* Handle description max length in dashboard sidebar

* Add red border to dashboard description box

* Add test
parent 5f4c2638
No related branches found
No related tags found
No related merge requests found
......@@ -4,6 +4,8 @@ import type {
HTMLAttributes,
Ref,
MouseEvent,
FocusEventHandler,
FocusEvent,
} from "react";
import { forwardRef, useCallback, useEffect, useState, useRef } from "react";
import { usePrevious } from "react-use";
......@@ -14,7 +16,7 @@ import { EditableTextArea, EditableTextRoot } from "./EditableText.styled";
export type EditableTextAttributes = Omit<
HTMLAttributes<HTMLDivElement>,
"onChange"
"onChange" | "onFocus" | "onBlur"
>;
export interface EditableTextProps extends EditableTextAttributes {
......@@ -26,8 +28,8 @@ export interface EditableTextProps extends EditableTextAttributes {
isDisabled?: boolean;
isMarkdown?: boolean;
onChange?: (value: string) => void;
onFocus?: () => void;
onBlur?: () => void;
onFocus?: FocusEventHandler<HTMLTextAreaElement>;
onBlur?: FocusEventHandler<HTMLTextAreaElement>;
"data-testid"?: string;
}
......@@ -72,18 +74,21 @@ const EditableText = forwardRef(function EditableText(
}
}, [isInFocus, isMarkdown]);
const handleBlur = useCallback(() => {
setIsInFocus(false);
const handleBlur = useCallback(
(event: FocusEvent<HTMLTextAreaElement>) => {
setIsInFocus(false);
if (!isOptional && !inputValue) {
setInputValue(submitValue);
} else if (inputValue !== submitValue && submitOnBlur.current) {
setSubmitValue(inputValue);
onChange?.(inputValue);
}
if (!isOptional && !inputValue) {
setInputValue(submitValue);
} else if (inputValue !== submitValue && submitOnBlur.current) {
setSubmitValue(inputValue);
onChange?.(inputValue);
}
onBlur?.();
}, [inputValue, submitValue, isOptional, onChange, onBlur, setIsInFocus]);
onBlur?.(event);
},
[inputValue, submitValue, isOptional, onChange, onBlur, setIsInFocus],
);
const handleChange = useCallback(
(event: ChangeEvent<HTMLTextAreaElement>) => {
......
import { css } from "@emotion/react";
import styled from "@emotion/styled";
import EditableText from "metabase/core/components/EditableText";
......@@ -61,3 +62,15 @@ export const ContentSection = styled.div`
export const DescriptionHeader = styled.h3`
margin-bottom: 0.5rem;
`;
export const EditableDescription = styled(EditableText)<{ hasError?: boolean }>`
${props =>
props.hasError &&
css`
border-color: var(--mb-color-error);
&:hover {
border-color: var(--mb-color-error);
}
`}
`;
import type { Dispatch, SetStateAction } from "react";
import type { Dispatch, FocusEvent, SetStateAction } from "react";
import { useCallback, useState } from "react";
import { t } from "ttag";
......@@ -6,24 +6,25 @@ import ErrorBoundary from "metabase/ErrorBoundary";
import { Timeline } from "metabase/common/components/Timeline";
import { getTimelineEvents } from "metabase/common/components/Timeline/utils";
import { useRevisionListQuery } from "metabase/common/hooks";
import EditableText from "metabase/core/components/EditableText";
import {
revertToRevision,
toggleAutoApplyFilters,
updateDashboard,
} from "metabase/dashboard/actions";
import { DASHBOARD_DESCRIPTION_MAX_LENGTH } from "metabase/dashboard/constants";
import { isDashboardCacheable } from "metabase/dashboard/utils";
import { useUniqueId } from "metabase/hooks/use-unique-id";
import { useDispatch, useSelector } from "metabase/lib/redux";
import { PLUGIN_CACHING } from "metabase/plugins";
import { getUser } from "metabase/selectors/user";
import { Stack, Switch } from "metabase/ui";
import { Text, Stack, Switch } from "metabase/ui";
import type { Dashboard } from "metabase-types/api";
import {
ContentSection,
DashboardInfoSidebarRoot,
DescriptionHeader,
EditableDescription,
HistoryHeader,
} from "./DashboardInfoSidebar.styled";
......@@ -75,6 +76,8 @@ const DashboardInfoSidebarBody = ({
setDashboardAttribute,
setPage,
}: DashboardSidebarPageProps) => {
const [descriptionError, setDescriptionError] = useState<string | null>(null);
const { data: revisions } = useRevisionListQuery({
query: { model_type: "dashboard", model_id: dashboard.id },
});
......@@ -84,12 +87,25 @@ const DashboardInfoSidebarBody = ({
const handleDescriptionChange = useCallback(
(description: string) => {
setDashboardAttribute?.("description", description);
dispatch(updateDashboard({ attributeNames: ["description"] }));
if (description.length <= DASHBOARD_DESCRIPTION_MAX_LENGTH) {
setDashboardAttribute?.("description", description);
dispatch(updateDashboard({ attributeNames: ["description"] }));
}
},
[dispatch, setDashboardAttribute],
);
const handleDescriptionBlur = useCallback(
(event: FocusEvent<HTMLTextAreaElement>) => {
if (event.target.value.length > DASHBOARD_DESCRIPTION_MAX_LENGTH) {
setDescriptionError(
t`Must be ${DASHBOARD_DESCRIPTION_MAX_LENGTH} characters or less`,
);
}
},
[],
);
const handleToggleAutoApplyFilters = useCallback(
(isAutoApplyingFilters: boolean) => {
dispatch(toggleAutoApplyFilters(isAutoApplyingFilters));
......@@ -107,17 +123,25 @@ const DashboardInfoSidebarBody = ({
<>
<ContentSection>
<DescriptionHeader>{t`About`}</DescriptionHeader>
<EditableText
<EditableDescription
initialValue={dashboard.description}
isDisabled={!canWrite}
onChange={handleDescriptionChange}
onFocus={() => setDescriptionError("")}
onBlur={handleDescriptionBlur}
isOptional
isMultiline
isMarkdown
hasError={!!descriptionError}
placeholder={t`Add description`}
key={`dashboard-description-${dashboard.description}`}
style={{ fontSize: ".875rem" }}
/>
{!!descriptionError && (
<Text color="error" size="xs" mt="xs">
{descriptionError}
</Text>
)}
</ContentSection>
{!dashboard.archived && (
......
......@@ -28,6 +28,11 @@ function setup({ dashboard = createMockDashboard() }: SetupOpts = {}) {
};
}
jest.mock("metabase/dashboard/constants", () => ({
...jest.requireActual("metabase/dashboard/constants"),
DASHBOARD_DESCRIPTION_MAX_LENGTH: 20,
}));
describe("DashboardInfoSidebar", () => {
it("should render the component", () => {
setup();
......@@ -51,6 +56,28 @@ describe("DashboardInfoSidebar", () => {
);
});
it("should validate description length", async () => {
const expectedErrorMessage = "Must be 20 characters or less";
const { setDashboardAttribute } = setup();
await userEvent.click(screen.getByTestId("editable-text"));
await userEvent.type(
screen.getByPlaceholderText("Add description"),
"in incididunt incididunt laboris ut elit culpa sit dolor amet",
);
await userEvent.tab();
expect(screen.getByText(expectedErrorMessage)).toBeInTheDocument();
await userEvent.click(screen.getByTestId("editable-text"));
expect(screen.queryByText(expectedErrorMessage)).not.toBeInTheDocument();
await userEvent.tab();
expect(screen.getByText(expectedErrorMessage)).toBeInTheDocument();
expect(setDashboardAttribute).not.toHaveBeenCalled();
});
it("should allow to clear description", async () => {
const { setDashboardAttribute } = setup({
dashboard: createMockDashboard({ description: "some description" }),
......
......@@ -5,6 +5,8 @@ import type {
import type { EmbedDisplayParams } from "./types";
export const DASHBOARD_DESCRIPTION_MAX_LENGTH = 1500;
export const SIDEBAR_NAME: Record<DashboardSidebarName, DashboardSidebarName> =
{
addQuestion: "addQuestion",
......
......@@ -23,13 +23,17 @@ import * as Errors from "metabase/lib/errors";
import type { CollectionId, Dashboard } from "metabase-types/api";
import { DashboardCopyModalShallowCheckboxLabel } from "../components/DashboardCopyModal/DashboardCopyModalShallowCheckboxLabel/DashboardCopyModalShallowCheckboxLabel";
import { DASHBOARD_DESCRIPTION_MAX_LENGTH } from "../constants";
const DASHBOARD_SCHEMA = Yup.object({
name: Yup.string()
.required(Errors.required)
.max(100, Errors.maxLength)
.default(""),
description: Yup.string().nullable().max(255, Errors.maxLength).default(null),
description: Yup.string()
.nullable()
.max(DASHBOARD_DESCRIPTION_MAX_LENGTH, Errors.maxLength)
.default(null),
collection_id: Yup.number().nullable().default(null),
is_shallow_copy: Yup.boolean().default(false),
});
......
......@@ -20,12 +20,17 @@ import * as Errors from "metabase/lib/errors";
import type { CollectionId, Dashboard } from "metabase-types/api";
import type { State } from "metabase-types/store";
import { DASHBOARD_DESCRIPTION_MAX_LENGTH } from "../constants";
const DASHBOARD_SCHEMA = Yup.object({
name: Yup.string()
.required(Errors.required)
.max(100, Errors.maxLength)
.default(""),
description: Yup.string().nullable().max(255, Errors.maxLength).default(null),
description: Yup.string()
.nullable()
.max(DASHBOARD_DESCRIPTION_MAX_LENGTH, Errors.maxLength)
.default(null),
collection_id: Yup.number().nullable(),
});
......
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