Skip to content
Snippets Groups Projects
Unverified Commit 00096b41 authored by Alexander Polyankin's avatar Alexander Polyankin Committed by GitHub
Browse files

Add tooltip to verify icon for questions (#23761)

parent 31e5f3a7
No related branches found
No related tags found
No related merge requests found
Showing
with 197 additions and 17 deletions
......@@ -7,7 +7,7 @@ import Icon from "metabase/components/Icon";
import { color, alpha } from "metabase/lib/colors";
import { getUser } from "metabase/selectors/user";
import { getRelativeTimeAbbreviated } from "metabase/lib/time";
import { getRelativeTime } from "metabase/lib/time";
import {
getTextForReviewBanner,
getIconForReview,
......@@ -54,9 +54,7 @@ export function ModerationReviewBanner({
moderator,
currentUser,
);
const relativeCreationTime = getRelativeTimeAbbreviated(
moderationReview.created_at,
);
const relativeCreationTime = getRelativeTime(moderationReview.created_at);
const { name: iconName, color: iconColor } =
getIconForReview(moderationReview);
......
export { default } from "./ModerationReviewBanner";
import styled from "@emotion/styled";
import { color } from "metabase/lib/colors";
export const TooltipTime = styled.time`
color: ${color("text-medium")};
font-size: 0.875em;
`;
import React from "react";
import { color } from "metabase/lib/colors";
import { getRelativeTime } from "metabase/lib/time";
import Icon from "metabase/components/Icon";
import Tooltip from "metabase/components/Tooltip";
import { ModerationReview, User } from "metabase-types/api";
import { getIconForReview, getModeratorDisplayText } from "../../service";
import { TooltipTime } from "./ModerationReviewIcon.styled";
export interface ModerationReviewIconProps {
review: ModerationReview;
moderator?: User;
currentUser: User;
}
const ModerationReviewIcon = ({
review,
moderator,
currentUser,
}: ModerationReviewIconProps): JSX.Element => {
const { name: iconName, color: iconColor } = getIconForReview(review);
const tooltip = moderator && (
<div>
<div>{getModeratorDisplayText(moderator, currentUser)}</div>
<TooltipTime dateTime={review.created_at}>
{getRelativeTime(review.created_at)}
</TooltipTime>
</div>
);
return (
<Tooltip tooltip={tooltip}>
<Icon name={iconName} color={color(iconColor)} />
</Tooltip>
);
};
export default ModerationReviewIcon;
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import {
createMockModerationReview,
createMockUser,
} from "metabase-types/api/mocks";
import ModerationReviewIcon, {
ModerationReviewIconProps,
} from "./ModerationReviewIcon";
describe("ModerationReviewIcon", () => {
beforeEach(() => {
jest.useFakeTimers();
jest.setSystemTime(new Date(2022, 1, 7));
});
afterEach(() => {
jest.useRealTimers();
});
it("should render correctly when moderator is loading", () => {
const props = getProps();
render(<ModerationReviewIcon {...props} />);
expect(screen.getByLabelText("verified icon")).toBeInTheDocument();
});
it("should show a tooltip on hover when moderator is loaded", () => {
const props = getProps({
review: createMockModerationReview({
moderator_id: 1,
created_at: "2021-01-01T20:10:30.200",
}),
moderator: createMockUser({ id: 1 }),
currentUser: createMockUser({ id: 1 }),
});
render(<ModerationReviewIcon {...props} />);
userEvent.hover(screen.getByLabelText("verified icon"));
expect(screen.getByText("You verified this")).toBeInTheDocument();
expect(screen.getByText("a year ago")).toBeInTheDocument();
});
});
const getProps = (
opts?: Partial<ModerationReviewIconProps>,
): ModerationReviewIconProps => ({
review: createMockModerationReview(),
currentUser: createMockUser(),
...opts,
});
export { default } from "./ModerationReviewIcon";
export { default } from "./ModerationStatusIcon";
export { default } from "./QuestionModerationButton";
import React from "react";
import Question from "metabase-lib/lib/Question";
import { getLatestModerationReview } from "../../service";
import ModerationReviewIcon from "../../containers/ModerationReviewIcon";
export interface QuestionModerationIconProps {
question: Question;
}
const QuestionModerationIcon = ({
question,
}: QuestionModerationIconProps): JSX.Element | null => {
const review = getLatestModerationReview(question.getModerationReviews());
if (review) {
return <ModerationReviewIcon review={review} />;
} else {
return null;
}
};
export default QuestionModerationIcon;
export { default } from "./QuestionModerationIcon";
export { default } from "./QuestionModerationSection";
import { connect } from "react-redux";
import _ from "underscore";
import Users from "metabase/entities/users";
import { getUser } from "metabase/selectors/user";
import { ModerationReview } from "metabase-types/api";
import { State } from "metabase-types/store";
import ModerationReviewIcon from "../../components/ModerationReviewIcon";
interface ModerationReviewIconProps {
review: ModerationReview;
}
const mapStateToProps = (state: State) => ({
currentUser: getUser(state),
});
const userProps = {
id: (state: State, props: ModerationReviewIconProps) =>
props.review.moderator_id,
entityAlias: "moderator",
loadingAndErrorWrapper: false,
};
export default _.compose(
Users.load(userProps),
connect(mapStateToProps),
)(ModerationReviewIcon);
export { default } from "./ModerationReviewIcon";
......@@ -3,14 +3,14 @@ import { t } from "ttag";
import { PLUGIN_MODERATION } from "metabase/plugins";
import { hasPremiumFeature } from "metabase-enterprise/settings";
import QuestionModerationSection from "./components/QuestionModerationSection/QuestionModerationSection";
import QuestionModerationButton from "./components/QuestionModerationButton/QuestionModerationButton";
import ModerationReviewBanner from "./components/ModerationReviewBanner/ModerationReviewBanner";
import ModerationStatusIcon from "./components/ModerationStatusIcon/ModerationStatusIcon";
import QuestionModerationIcon from "./components/QuestionModerationIcon";
import QuestionModerationSection from "./components/QuestionModerationSection";
import QuestionModerationButton from "./components/QuestionModerationButton";
import ModerationReviewBanner from "./components/ModerationReviewBanner";
import ModerationStatusIcon from "./components/ModerationStatusIcon";
import {
MODERATION_STATUS,
getStatusIconForQuestion,
getStatusIcon,
getModerationTimelineEvents,
verifyItem,
......@@ -22,11 +22,11 @@ import {
if (hasPremiumFeature("content_management")) {
Object.assign(PLUGIN_MODERATION, {
isEnabled: () => true,
QuestionModerationIcon,
QuestionModerationSection,
QuestionModerationButton,
ModerationReviewBanner,
ModerationStatusIcon,
getStatusIconForQuestion,
getStatusIcon,
getModerationTimelineEvents,
getMenuItems: (model, isModerator, reload) => {
......
......@@ -62,11 +62,10 @@ export function getTextForReviewBanner(
moderator,
currentUser,
) {
const moderatorName = getModeratorDisplayName(moderator, currentUser);
const { status } = moderationReview;
if (status === "verified") {
const bannerText = t`${moderatorName} verified this`;
const bannerText = getModeratorDisplayText(moderator, currentUser);
const tooltipText = t`Remove verification`;
return { bannerText, tooltipText };
}
......@@ -74,19 +73,24 @@ export function getTextForReviewBanner(
return {};
}
function getModeratorDisplayName(user, currentUser) {
const { id: userId, common_name } = user || {};
export function getModeratorDisplayName(moderator, currentUser) {
const { id: moderatorId, common_name } = moderator || {};
const { id: currentUserId } = currentUser || {};
if (currentUserId != null && userId === currentUserId) {
if (currentUserId != null && moderatorId === currentUserId) {
return t`You`;
} else if (userId != null) {
} else if (moderatorId != null) {
return common_name;
} else {
return t`A moderator`;
}
}
export function getModeratorDisplayText(moderator, currentUser) {
const moderatorName = getModeratorDisplayName(moderator, currentUser);
return t`${moderatorName} verified this`;
}
// a `status` of `null` represents the removal of a review, since we can't delete reviews
export function isRemovedReviewStatus(status) {
return String(status) === "null";
......
export type VisualizationSettings = {
[key: string]: any;
};
export interface ModerationReview {
moderator_id: number;
status: ModerationReviewStatus | null;
created_at: string;
most_recent: boolean;
}
export type ModerationReviewStatus = "verified";
export * from "./activity";
export * from "./automagic-dashboards";
export * from "./bookmark";
export * from "./card";
export * from "./collection";
export * from "./dashboard";
export * from "./database";
......
import { ModerationReview } from "metabase-types/api";
export const createMockModerationReview = (
opts?: Partial<ModerationReview>,
): ModerationReview => ({
moderator_id: 1,
status: "verified",
created_at: "2015-01-01T20:10:30.200",
most_recent: true,
...opts,
});
export * from "./activity";
export * from "./automagic-dashboards";
export * from "./card";
export * from "./collection";
export * from "./dashboard";
export * from "./database";
......
......@@ -120,11 +120,11 @@ export const PLUGIN_COLLECTION_COMPONENTS = {
export const PLUGIN_MODERATION = {
isEnabled: () => false,
QuestionModerationIcon: PluginPlaceholder,
QuestionModerationSection: PluginPlaceholder,
QuestionModerationButton: PluginPlaceholder,
ModerationReviewBanner: PluginPlaceholder,
ModerationStatusIcon: PluginPlaceholder,
getStatusIconForQuestion: object,
getStatusIcon: object,
getModerationTimelineEvents: array,
getMenuItems: (
......
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