diff --git a/enterprise/frontend/src/metabase-enterprise/moderation/components/ModerationReviewBanner/ModerationReviewBanner.jsx b/enterprise/frontend/src/metabase-enterprise/moderation/components/ModerationReviewBanner/ModerationReviewBanner.tsx similarity index 64% rename from enterprise/frontend/src/metabase-enterprise/moderation/components/ModerationReviewBanner/ModerationReviewBanner.jsx rename to enterprise/frontend/src/metabase-enterprise/moderation/components/ModerationReviewBanner/ModerationReviewBanner.tsx index 27b494827d0cfa98c968120eafbce7b4e8332cee..b94e11aa135bb8ae5c2acc20c29cea032bdf1f74 100644 --- a/enterprise/frontend/src/metabase-enterprise/moderation/components/ModerationReviewBanner/ModerationReviewBanner.jsx +++ b/enterprise/frontend/src/metabase-enterprise/moderation/components/ModerationReviewBanner/ModerationReviewBanner.tsx @@ -1,52 +1,44 @@ -import PropTypes from "prop-types"; import { connect } from "react-redux"; import _ from "underscore"; import Users from "metabase/entities/users"; -import { color, alpha } from "metabase/lib/colors"; +import { alpha, color } from "metabase/lib/colors"; import { getRelativeTime } from "metabase/lib/time"; import { getUser } from "metabase/selectors/user"; import { Icon } from "metabase/ui"; import { - getTextForReviewBanner, getIconForReview, + getTextForReviewBanner, } from "metabase-enterprise/moderation/service"; +import type { ModerationReview, User } from "metabase-types/api"; import { Container, Text, - Time, TextContainer, + Time, } from "./ModerationReviewBanner.styled"; const ICON_BUTTON_SIZE = 16; -const mapStateToProps = (state, props) => ({ +const mapStateToProps = (state: any, _props: any) => ({ currentUser: getUser(state), }); -export default _.compose( - Users.load({ - id: (state, props) => props.moderationReview.moderator_id, - loadingAndErrorWrapper: false, - }), - connect(mapStateToProps), -)(ModerationReviewBanner); - -ModerationReviewBanner.propTypes = { - moderationReview: PropTypes.object.isRequired, - user: PropTypes.object, - currentUser: PropTypes.object.isRequired, - onRemove: PropTypes.func, - className: PropTypes.func, -}; +interface ModerationReviewBannerProps { + moderationReview: ModerationReview; + user?: User | null; + currentUser: User; + onRemove?: () => void; + className?: string; +} -export function ModerationReviewBanner({ +export const ModerationReviewBanner = ({ moderationReview, - user: moderator, + user: moderator = null, currentUser, className, -}) { +}: ModerationReviewBannerProps) => { const { bannerText } = getTextForReviewBanner( moderationReview, moderator, @@ -57,7 +49,10 @@ export function ModerationReviewBanner({ getIconForReview(moderationReview); return ( - <Container backgroundColor={alpha(iconColor, 0.2)} className={className}> + <Container + style={{ backgroundColor: alpha(iconColor, 0.2) }} + className={className} + > <Icon name={iconName} color={color(iconColor)} size={ICON_BUTTON_SIZE} /> <TextContainer> <Text>{bannerText}</Text> @@ -67,4 +62,13 @@ export function ModerationReviewBanner({ </TextContainer> </Container> ); -} +}; + +// eslint-disable-next-line import/no-default-export -- deprecated usage +export default _.compose( + Users.load({ + id: (_state: any, props: any) => props.moderationReview.moderator_id, + loadingAndErrorWrapper: false, + }), + connect(mapStateToProps), +)(ModerationReviewBanner); diff --git a/enterprise/frontend/src/metabase-enterprise/moderation/components/ModerationReviewBanner/ModerationReviewBanner.unit.spec.js b/enterprise/frontend/src/metabase-enterprise/moderation/components/ModerationReviewBanner/ModerationReviewBanner.unit.spec.tsx similarity index 59% rename from enterprise/frontend/src/metabase-enterprise/moderation/components/ModerationReviewBanner/ModerationReviewBanner.unit.spec.js rename to enterprise/frontend/src/metabase-enterprise/moderation/components/ModerationReviewBanner/ModerationReviewBanner.unit.spec.tsx index ae7fb2bb3372c5720a03d3fc0daffe60acdceeab..1f66f13cff2ae3ad3ed7912dd6364c2bc3bfcb68 100644 --- a/enterprise/frontend/src/metabase-enterprise/moderation/components/ModerationReviewBanner/ModerationReviewBanner.unit.spec.js +++ b/enterprise/frontend/src/metabase-enterprise/moderation/components/ModerationReviewBanner/ModerationReviewBanner.unit.spec.tsx @@ -1,14 +1,17 @@ import { render, screen } from "@testing-library/react"; +import type { ModerationReview, User } from "metabase-types/api"; +import { createMockUser } from "metabase-types/api/mocks"; + import { ModerationReviewBanner } from "./ModerationReviewBanner"; -const moderationReview = { +const moderationReview: ModerationReview = { status: "verified", moderator_id: 1, - created_at: Date.now(), + created_at: Date.now().toString(), }; -const moderator = { id: 1, common_name: "Foo" }; -const currentUser = { id: 2, common_name: "Bar" }; +const moderator: User = createMockUser({ id: 1, common_name: "Foo" }); +const currentUser: User = createMockUser({ id: 2, common_name: "Bar" }); describe("ModerationReviewBanner", () => { it("should show text concerning the given review", () => { diff --git a/enterprise/frontend/src/metabase-enterprise/moderation/components/ModerationStatusIcon/ModerationStatusIcon.tsx b/enterprise/frontend/src/metabase-enterprise/moderation/components/ModerationStatusIcon/ModerationStatusIcon.tsx index 5872ded3638eaee3060790994e3e951b98814d68..80e5f7c4675347658efdf8d79476a14fe842924c 100644 --- a/enterprise/frontend/src/metabase-enterprise/moderation/components/ModerationStatusIcon/ModerationStatusIcon.tsx +++ b/enterprise/frontend/src/metabase-enterprise/moderation/components/ModerationStatusIcon/ModerationStatusIcon.tsx @@ -1,6 +1,6 @@ import { color } from "metabase/lib/colors"; import type { IconProps } from "metabase/ui"; -import { Icon } from "metabase/ui"; +import { FixedSizeIcon } from "metabase/ui"; import { getStatusIcon } from "metabase-enterprise/moderation/service"; type ModerationStatusIconProps = { @@ -13,8 +13,25 @@ export const ModerationStatusIcon = ({ filled = false, ...iconProps }: ModerationStatusIconProps) => { - const { name: iconName, color: iconColor } = getStatusIcon(status, filled); - return iconName ? ( - <Icon name={iconName} color={color(iconColor)} {...iconProps} /> - ) : null; + const { name: iconName, color: iconColor } = getStatusIcon( + status ?? null, + filled, + ); + if (!iconName) { + return null; + } + const reformattedIconProps = { + ...iconProps, + size: + typeof iconProps.size === "string" + ? parseInt(iconProps.size) + : iconProps.size, + }; + return ( + <FixedSizeIcon + name={iconName} + color={color(iconColor)} + {...reformattedIconProps} + /> + ); }; diff --git a/enterprise/frontend/src/metabase-enterprise/moderation/constants.js b/enterprise/frontend/src/metabase-enterprise/moderation/constants.js deleted file mode 100644 index 4b56960553e9daf94177fafa05386c7b25798d0b..0000000000000000000000000000000000000000 --- a/enterprise/frontend/src/metabase-enterprise/moderation/constants.js +++ /dev/null @@ -1,18 +0,0 @@ -export const MODERATION_STATUS = { - verified: "verified", -}; - -export const MODERATION_STATUS_ICONS = { - verified: { - name: "verified", - color: "brand", - }, - verified_filled: { - name: "verified_filled", - color: "brand", - }, - null: { - name: "close", - color: "text-light", - }, -}; diff --git a/enterprise/frontend/src/metabase-enterprise/moderation/constants.ts b/enterprise/frontend/src/metabase-enterprise/moderation/constants.ts new file mode 100644 index 0000000000000000000000000000000000000000..07beaa5291fa142d74d7ea8aeab2759ca3ab337b --- /dev/null +++ b/enterprise/frontend/src/metabase-enterprise/moderation/constants.ts @@ -0,0 +1,26 @@ +import type { ColorName } from "metabase/lib/colors/types"; +import type { IconName } from "metabase/ui"; + +export const MODERATION_STATUS = { + verified: "verified", +}; + +export const MODERATION_STATUS_ICONS: Map< + string | null, + { name: IconName; color: ColorName } +> = new Map(); + +MODERATION_STATUS_ICONS.set("verified", { + name: "verified", + color: "brand", +}); + +MODERATION_STATUS_ICONS.set("verified_filled", { + name: "verified_filled", + color: "brand", +}); + +MODERATION_STATUS_ICONS.set(null, { + name: "close", + color: "text-light", +}); diff --git a/enterprise/frontend/src/metabase-enterprise/moderation/service.js b/enterprise/frontend/src/metabase-enterprise/moderation/service.js deleted file mode 100644 index b63595ad9ca3cecb749c7979e4714c1f8c79915c..0000000000000000000000000000000000000000 --- a/enterprise/frontend/src/metabase-enterprise/moderation/service.js +++ /dev/null @@ -1,145 +0,0 @@ -import { t } from "ttag"; -import _ from "underscore"; - -import { ModerationReviewApi } from "metabase/services"; - -import { MODERATION_STATUS_ICONS } from "./constants"; - -export { MODERATION_STATUS } from "./constants"; - -export function verifyItem({ text, itemId, itemType }) { - return ModerationReviewApi.create({ - status: "verified", - moderated_item_id: itemId, - moderated_item_type: itemType, - text, - }); -} - -export function removeReview({ itemId, itemType }) { - return ModerationReviewApi.create({ - status: null, - moderated_item_id: itemId, - moderated_item_type: itemType, - }); -} - -const noIcon = {}; - -export function getStatusIcon(status, filled = false) { - if (isRemovedReviewStatus(status)) { - return noIcon; - } - - if (status === "verified" && filled) { - return MODERATION_STATUS_ICONS[`${status}_filled`]; - } - - return MODERATION_STATUS_ICONS[status] || noIcon; -} - -export function getIconForReview(review, options) { - return getStatusIcon(review?.status, options); -} - -// we only want the icon that represents the removal of a review in special cases, -// so you must ask for the icon explicitly -export function getRemovedReviewStatusIcon() { - return MODERATION_STATUS_ICONS[null]; -} - -export function getLatestModerationReview(reviews) { - const maybeReview = _.findWhere(reviews, { - most_recent: true, - }); - - // since we can't delete reviews, consider a most recent review with a status of null to mean there is no review - return isRemovedReviewStatus(maybeReview?.status) ? undefined : maybeReview; -} - -export function getStatusIconForQuestion(question) { - const reviews = question.getModerationReviews(); - const review = getLatestModerationReview(reviews); - return getIconForReview(review); -} - -export function getTextForReviewBanner( - moderationReview, - moderator, - currentUser, -) { - const { status } = moderationReview; - - if (status === "verified") { - const bannerText = getModeratorDisplayText(moderator, currentUser); - const tooltipText = t`Remove verification`; - return { bannerText, tooltipText }; - } - - return {}; -} - -export function getModeratorDisplayName(moderator, currentUser) { - const { id: moderatorId, common_name } = moderator || {}; - const { id: currentUserId } = currentUser || {}; - - if (currentUserId != null && moderatorId === currentUserId) { - return t`You`; - } 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 function isItemVerified(review) { - return review != null && review.status === "verified"; -} - -function getModerationReviewEventText(review, moderatorDisplayName) { - switch (review.status) { - case "verified": - return t`${moderatorDisplayName} verified this`; - case null: - return t`${moderatorDisplayName} removed verification`; - default: - return t`${moderatorDisplayName} changed status to ${review.status}`; - } -} - -export function getModerationTimelineEvents(reviews, usersById, currentUser) { - return reviews.map(review => { - const moderator = usersById[review.moderator_id]; - const moderatorDisplayName = getModeratorDisplayName( - moderator, - currentUser, - ); - const text = getModerationReviewEventText(review, moderatorDisplayName); - const icon = isRemovedReviewStatus(review.status) - ? getRemovedReviewStatusIcon() - : getIconForReview(review); - - return { - timestamp: new Date(review.created_at).toISOString(), - icon, - title: text, - }; - }); -} - -export const getQuestionIcon = card => { - return (card.model === "dataset" || card.type === "model") && - card.moderated_status === "verified" - ? { icon: "model_with_badge", tooltip: "Verified model" } - : null; -}; diff --git a/enterprise/frontend/src/metabase-enterprise/moderation/service.ts b/enterprise/frontend/src/metabase-enterprise/moderation/service.ts new file mode 100644 index 0000000000000000000000000000000000000000..c169977de3d49608d665d7fe7f098c0d3b6fd782 --- /dev/null +++ b/enterprise/frontend/src/metabase-enterprise/moderation/service.ts @@ -0,0 +1,192 @@ +import { c, t } from "ttag"; +import _ from "underscore"; + +import type { ColorName } from "metabase/lib/colors/types"; +import { ModerationReviewApi } from "metabase/services"; +import type { IconName } from "metabase/ui"; +import type Question from "metabase-lib/v1/Question"; +import type { ModerationReview, User } from "metabase-types/api"; + +import { MODERATION_STATUS_ICONS } from "./constants"; + +export { MODERATION_STATUS } from "./constants"; + +export function verifyItem({ + text, + itemId, + itemType, +}: { + text: string; + itemId: number; + itemType: string; +}) { + return ModerationReviewApi.create({ + status: "verified", + moderated_item_id: itemId, + moderated_item_type: itemType, + text, + }); +} + +export function removeReview({ + itemId, + itemType, +}: { + itemId: number; + itemType: string; +}) { + return ModerationReviewApi.create({ + status: null, + moderated_item_id: itemId, + moderated_item_type: itemType, + }); +} + +type NoIcon = Record<string, never>; + +const noIcon: NoIcon = {}; + +export const getStatusIcon = ( + status: string | null | undefined, + filled = false, +): { name: IconName; color: ColorName } | NoIcon => { + if (!status || isRemovedReviewStatus(status)) { + return noIcon; + } + + if (status === "verified" && filled) { + return MODERATION_STATUS_ICONS.get("verified_filled") || noIcon; + } + + return MODERATION_STATUS_ICONS.get(status) || noIcon; +}; + +export function getIconForReview(review: ModerationReview, options?: any) { + return getStatusIcon(review?.status, options); +} + +// we only want the icon that represents the removal of a review in special cases, +// so you must ask for the icon explicitly +export function getRemovedReviewStatusIcon() { + return MODERATION_STATUS_ICONS.get(null); +} + +export function getLatestModerationReview(reviews: ModerationReview[]) { + const maybeReview = _.findWhere(reviews, { + most_recent: true, + }); + if (!maybeReview) { + return undefined; + } + // since we can't delete reviews, consider a most recent review with a status of null to mean there is no review + return isRemovedReviewStatus(maybeReview.status) ? undefined : maybeReview; +} + +export const getStatusIconForQuestion = (question: Question) => { + const reviews = question.getModerationReviews(); + const review = getLatestModerationReview(reviews); + return review ? getIconForReview(review) : noIcon; +}; + +export const getTextForReviewBanner = ( + moderationReview: ModerationReview, + moderator: User | null, + currentUser: User | null, +) => { + const { status } = moderationReview; + + if (status === "verified") { + const bannerText = getModeratorDisplayText(moderator, currentUser); + const tooltipText = t`Remove verification`; + return { bannerText, tooltipText }; + } + + return {}; +}; + +export const getModeratorDisplayName = ( + moderator: User | null, + currentUser?: User | null, +) => { + const { id: moderatorId, common_name } = moderator || {}; + const { id: currentUserId } = currentUser || {}; + + if (currentUserId != null && moderatorId === currentUserId) { + return t`You`; + } else if (moderatorId != null && common_name) { + return common_name; + } else { + return t`A moderator`; + } +}; + +export const getModeratorDisplayText = ( + moderator: User | null, + currentUser: User | null, +) => { + const moderatorName = getModeratorDisplayName(moderator, currentUser); + return c("{0} is the name of a user").t`${moderatorName} verified this`; +}; + +// a `status` of `null` represents the removal of a review, since we can't delete reviews +export const isRemovedReviewStatus = (status: string | null) => { + return status === null; +}; + +export const isItemVerified = ( + review?: ModerationReview | undefined | null, +) => { + return review != null && review.status === "verified"; +}; + +const getModerationReviewEventText = ( + review: ModerationReview, + moderatorDisplayName: string, +) => { + switch (review.status) { + case "verified": + return c("{0} is the name of a user") + .t`${moderatorDisplayName} verified this`; + case null: + return c("{0} is the name of a user") + .t`${moderatorDisplayName} removed verification`; + default: + return c("{0} is the name of a user, {1} is the status of a review") + .t`${moderatorDisplayName} changed status to ${review.status}`; + } +}; + +export function getModerationTimelineEvents( + reviews: ModerationReview[], + usersById: Record<number, User>, + currentUser?: User, +) { + return reviews.map(review => { + const moderator = review.moderator_id + ? usersById[review.moderator_id] + : null; + const moderatorDisplayName = getModeratorDisplayName( + moderator, + currentUser, + ); + const text = getModerationReviewEventText(review, moderatorDisplayName); + const icon = isRemovedReviewStatus(review.status) + ? getRemovedReviewStatusIcon() + : getIconForReview(review); + + return { + timestamp: review.created_at + ? new Date(review.created_at).toISOString() + : null, + icon, + title: text, + }; + }); +} + +export const getQuestionIcon = (card: any) => { + return (card.model === "dataset" || card.type === "model") && + card.moderated_status === "verified" + ? { icon: "model_with_badge" as IconName, tooltip: "Verified model" } + : null; +}; diff --git a/enterprise/frontend/src/metabase-enterprise/moderation/service.unit.spec.js b/enterprise/frontend/src/metabase-enterprise/moderation/service.unit.spec.ts similarity index 70% rename from enterprise/frontend/src/metabase-enterprise/moderation/service.unit.spec.js rename to enterprise/frontend/src/metabase-enterprise/moderation/service.unit.spec.ts index 11a0c2a6b15c3035129e39d414dd981e2168c257..ead73bcefa66df503fd0ed97ce4c7d51074ec40f 100644 --- a/enterprise/frontend/src/metabase-enterprise/moderation/service.unit.spec.js +++ b/enterprise/frontend/src/metabase-enterprise/moderation/service.unit.spec.ts @@ -1,4 +1,10 @@ import { ModerationReviewApi } from "metabase/services"; +import type Question from "metabase-lib/v1/Question"; +import type { ModerationReview, User } from "metabase-types/api"; +import { + createMockModerationReview, + createMockUser, +} from "metabase-types/api/mocks"; import { verifyItem, @@ -93,15 +99,21 @@ describe("moderation/service", () => { describe("getIconForReview", () => { it("should return icon name/color for given review", () => { - expect(getIconForReview({ status: "verified" })).toEqual( - getStatusIcon("verified"), - ); + expect( + getIconForReview(createMockModerationReview({ status: "verified" })), + ).toEqual(getStatusIcon("verified")); }); }); describe("getTextForReviewBanner", () => { it("should return text for a verified review", () => { - expect(getTextForReviewBanner({ status: "verified" })).toEqual({ + expect( + getTextForReviewBanner( + createMockModerationReview({ status: "verified" }), + null, + null, + ), + ).toEqual({ bannerText: "A moderator verified this", tooltipText: "Remove verification", }); @@ -110,12 +122,12 @@ describe("moderation/service", () => { it("should include the moderator name", () => { expect( getTextForReviewBanner( - { status: "verified" }, - { + createMockModerationReview({ status: "verified" }), + createMockUser({ common_name: "Foo", id: 1, - }, - { id: 2 }, + }), + createMockUser({ id: 2 }), ), ).toEqual({ bannerText: "Foo verified this", @@ -126,12 +138,12 @@ describe("moderation/service", () => { it("should handle the moderator being the current user", () => { expect( getTextForReviewBanner( - { status: "verified" }, - { + createMockModerationReview({ status: "verified" }), + createMockUser({ common_name: "Foo", id: 1, - }, - { id: 1 }, + }), + createMockUser({ id: 1 }), ), ).toEqual({ bannerText: "You verified this", @@ -142,11 +154,15 @@ describe("moderation/service", () => { describe("isItemVerified", () => { it("should return true for a verified review", () => { - expect(isItemVerified({ status: "verified" })).toBe(true); + expect( + isItemVerified(createMockModerationReview({ status: "verified" })), + ).toBe(true); }); it("should return false for a null review", () => { - expect(isItemVerified({ status: null })).toBe(false); + expect(isItemVerified(createMockModerationReview({ status: null }))).toBe( + false, + ); }); it("should return false for no review", () => { @@ -156,24 +172,30 @@ describe("moderation/service", () => { describe("getLatestModerationReview", () => { it("should return the review flagged as most recent", () => { - const reviews = [ - { id: 1, status: "verified" }, - { id: 2, status: "verified", most_recent: true }, - { id: 3, status: null }, + const reviews: ModerationReview[] = [ + { moderator_id: 0, created_at: "", status: "verified" }, + { + moderator_id: 0, + created_at: "", + status: "verified", + most_recent: true, + }, + { moderator_id: 0, created_at: "", status: null }, ]; expect(getLatestModerationReview(reviews)).toEqual({ - id: 2, + moderator_id: 0, + created_at: "", status: "verified", most_recent: true, }); }); it("should return undefined when there is no review flagged as most recent", () => { - const reviews = [ - { id: 1, status: "verified" }, - { id: 2, status: "verified" }, - { id: 3, status: null }, + const reviews: ModerationReview[] = [ + { moderator_id: 0, created_at: "", status: "verified" }, + { moderator_id: 0, created_at: "", status: "verified" }, + { moderator_id: 0, created_at: "", status: null }, ]; expect(getLatestModerationReview(reviews)).toEqual(undefined); @@ -181,10 +203,10 @@ describe("moderation/service", () => { }); it("should return undefined when there is a review with a status of null flagged as most recent", () => { - const reviews = [ - { id: 1, status: "verified" }, - { id: 2, status: "verified" }, - { id: 3, status: null, most_recent: true }, + const reviews: ModerationReview[] = [ + { moderator_id: 0, created_at: "", status: "verified" }, + { moderator_id: 0, created_at: "", status: "verified" }, + { moderator_id: 0, created_at: "", status: null, most_recent: true }, ]; expect(getLatestModerationReview(reviews)).toEqual(undefined); @@ -199,31 +221,34 @@ describe("moderation/service", () => { { id: 2, status: "verified", most_recent: true }, { id: 3, status: null }, ], - }; - - expect(getStatusIconForQuestion(questionWithReviews)).toEqual( - getStatusIcon("verified"), - ); + } as unknown as Question; + + const { color: actualColor, name: actualName } = + getStatusIconForQuestion(questionWithReviews) || {}; + const { color: expectedColor, name: expectedName } = + getStatusIcon("verified"); + expect(expectedColor).toEqual(actualColor); + expect(expectedName).toEqual(actualName); }); it("should return undefined vals for no review", () => { const questionWithNoMostRecentReview = { getModerationReviews: () => [ - { id: 1, status: "verified" }, - { id: 2, status: "verified" }, - { id: 3, status: null, most_recent: true }, + { moderator_id: 0, created_at: "", status: "verified" }, + { moderator_id: 0, created_at: "", status: "verified" }, + { moderator_id: 0, created_at: "", status: null, most_recent: true }, ], - }; + } as unknown as Question; const questionWithNoReviews = { getModerationReviews: () => [], - }; + } as unknown as Question; const questionWithUndefinedReviews = { getModerationReviews: () => undefined, - }; + } as unknown as Question; - const noIcon = { name: undefined, color: undefined }; + const noIcon = {}; expect(getStatusIconForQuestion(questionWithNoMostRecentReview)).toEqual( noIcon, @@ -237,25 +262,23 @@ describe("moderation/service", () => { describe("getModerationTimelineEvents", () => { it("should return the moderation timeline events", () => { - const reviews = [ + const reviews: ModerationReview[] = [ { - id: 1, status: "verified", created_at: "2018-01-01T00:00:00.000Z", moderator_id: 1, }, { - id: 2, status: null, created_at: "2018-01-02T00:00:00.000Z", moderator_id: 123, }, ]; - const usersById = { - 1: { + const usersById: Record<number, User> = { + 1: createMockUser({ id: 1, common_name: "Foo", - }, + }), }; expect(getModerationTimelineEvents(reviews, usersById)).toEqual([ diff --git a/frontend/src/metabase-types/api/card.ts b/frontend/src/metabase-types/api/card.ts index 650fbaee37cf69f441f1c80f7cf06877b7106cee..85c161f8487ce44ebb6c03a753ae8f310cd2008a 100644 --- a/frontend/src/metabase-types/api/card.ts +++ b/frontend/src/metabase-types/api/card.ts @@ -196,14 +196,14 @@ export type VisualizationSettings = { }; export interface ModerationReview { + status: ModerationReviewStatus; moderator_id: number; - status: ModerationReviewStatus | null; created_at: string; - most_recent: boolean; + most_recent?: boolean; } export type CardId = number; -export type ModerationReviewStatus = "verified"; +export type ModerationReviewStatus = "verified" | null; export type CardFilterOption = | "all"