diff --git a/e2e/test/scenarios/dashboard-cards/reproductions/15993-click-behavior-question-with-filter-causes-error.cy.spec.js b/e2e/test/scenarios/dashboard-cards/reproductions/15993-click-behavior-question-with-filter-causes-error.cy.spec.js index 7774f43d0f491ddbd79b9ef93d4d1dddf5c7e385..d2d3ba2e98374867c249b6e4f90b283ee55cbc2b 100644 --- a/e2e/test/scenarios/dashboard-cards/reproductions/15993-click-behavior-question-with-filter-causes-error.cy.spec.js +++ b/e2e/test/scenarios/dashboard-cards/reproductions/15993-click-behavior-question-with-filter-causes-error.cy.spec.js @@ -39,7 +39,7 @@ describe("issue 15993", () => { }); // Drill-through - cy.findAllByTestId("cell-data").get(".link").contains("0").realClick(); + cy.findAllByTestId("cell-data").contains("0").realClick(); // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage cy.contains("117.03").should("not.exist"); // Total for the order in which quantity wasn't 0 diff --git a/enterprise/frontend/src/metabase-enterprise/advanced_permissions/components/ImpersonationModal/ImpersonationModalView.tsx b/enterprise/frontend/src/metabase-enterprise/advanced_permissions/components/ImpersonationModal/ImpersonationModalView.tsx index a341d79b7473d1bebce53449c327e479ed7e4fc7..df2efce4bf9e9e5aa6c3b5419f5a325c0e542c51 100644 --- a/enterprise/frontend/src/metabase-enterprise/advanced_permissions/components/ImpersonationModal/ImpersonationModalView.tsx +++ b/enterprise/frontend/src/metabase-enterprise/advanced_permissions/components/ImpersonationModal/ImpersonationModalView.tsx @@ -10,6 +10,7 @@ import FormFooter from "metabase/core/components/FormFooter"; import FormSelect from "metabase/core/components/FormSelect"; import FormSubmitButton from "metabase/core/components/FormSubmitButton"; import Link from "metabase/core/components/Link/Link"; +import CS from "metabase/css/core/index.css"; import { Form, FormProvider } from "metabase/forms"; import * as Errors from "metabase/lib/errors"; import MetabaseSettings from "metabase/lib/settings"; @@ -93,7 +94,7 @@ export const ImpersonationModalView = ({ <ImpersonationDescription> {modalMessage}{" "} <ExternalLink - className="link" + className={CS.link} // eslint-disable-next-line no-unconditional-metabase-links-render -- Admin settings href={MetabaseSettings.docsUrl("permissions/data")} >{t`Learn More`}</ExternalLink> diff --git a/enterprise/frontend/src/metabase-enterprise/advanced_permissions/components/ImpersonationWarning/ImpersonationWarning.tsx b/enterprise/frontend/src/metabase-enterprise/advanced_permissions/components/ImpersonationWarning/ImpersonationWarning.tsx index 1fedad4415bc8924f68526474b9e7f9dc01cbef8..3bc90ddc01d9aa90a31ab924790e281fd833a129 100644 --- a/enterprise/frontend/src/metabase-enterprise/advanced_permissions/components/ImpersonationWarning/ImpersonationWarning.tsx +++ b/enterprise/frontend/src/metabase-enterprise/advanced_permissions/components/ImpersonationWarning/ImpersonationWarning.tsx @@ -2,6 +2,7 @@ import { t, jt } from "ttag"; import { BoldCode } from "metabase/components/Code"; import Link from "metabase/core/components/Link"; +import CS from "metabase/css/core/index.css"; import * as Urls from "metabase/lib/urls"; import { isEmpty } from "metabase/lib/validate"; import type Database from "metabase-lib/v1/metadata/Database"; @@ -61,7 +62,7 @@ export const ImpersonationWarning = ({ <ImpersonationAlert icon="warning" variant="warning"> {isEmpty(databaseUser) ? emptyText : warningText}{" "} <Link - className="link" + className={CS.link} to={Urls.editDatabase(database.id) + (databaseUser ? "#user" : "")} >{t`Edit settings`}</Link> </ImpersonationAlert> diff --git a/frontend/src/metabase/admin/databases/components/DatabaseList/DatabaseList.jsx b/frontend/src/metabase/admin/databases/components/DatabaseList/DatabaseList.jsx index 3eb2a55d1ba20772916e5524001f441b2b1e1c4e..d4d693150fc712dacf46344dfee211dca47ec892 100644 --- a/frontend/src/metabase/admin/databases/components/DatabaseList/DatabaseList.jsx +++ b/frontend/src/metabase/admin/databases/components/DatabaseList/DatabaseList.jsx @@ -120,7 +120,7 @@ export default class DatabaseList extends Component { )} <Link to={"/admin/databases/" + database.id} - className="text-bold link" + className={cx("text-bold", CS.link)} > {database.name} </Link> @@ -153,7 +153,7 @@ export default class DatabaseList extends Component { })} > {isAddingSampleDatabase ? ( - <span className="text-light no-decoration"> + <span className={cx("text-light", CS.noDecoration)}> {t`Restoring the sample database...`} </span> ) : ( diff --git a/frontend/src/metabase/admin/people/components/AddRow.jsx b/frontend/src/metabase/admin/people/components/AddRow.jsx index 7cb53103de3f65876a2b282ec49a1b99a0c4ddbf..5076d658c484bb7d51831974854e583c0dc8440b 100644 --- a/frontend/src/metabase/admin/people/components/AddRow.jsx +++ b/frontend/src/metabase/admin/people/components/AddRow.jsx @@ -44,7 +44,7 @@ export const AddRow = forwardRef(function AddRow( onKeyDown={onKeyDown} onChange={onChange} /> - <span className="link no-decoration cursor-pointer" onClick={onCancel}> + <span className={CS.link} onClick={onCancel}> {t`Cancel`} </span> <button diff --git a/frontend/src/metabase/admin/people/components/GroupsListing.jsx b/frontend/src/metabase/admin/people/components/GroupsListing.jsx index 16ee454937a33d4c10695551d28ad7a5cafc54e6..11cc11c0e724f5ab573428a024cce799e0256462 100644 --- a/frontend/src/metabase/admin/people/components/GroupsListing.jsx +++ b/frontend/src/metabase/admin/people/components/GroupsListing.jsx @@ -164,10 +164,7 @@ function EditingGroupRow({ </td> <td /> <td className="text-right"> - <span - className="link no-decoration cursor-pointer" - onClick={onCancelClicked} - >{t`Cancel`}</span> + <span className={CS.link} onClick={onCancelClicked}>{t`Cancel`}</span> <button className={cx(ButtonsS.Button, CS.ml2, { [ButtonsS.ButtonPrimary]: textIsValid && textHasChanged, @@ -213,7 +210,7 @@ function GroupRow({ <td> <Link to={"/admin/people/groups/" + group.id} - className={cx("link", CS.noDecoration, CS.flex, CS.alignCenter)} + className={cx(CS.link, CS.flex, CS.alignCenter)} > <span className="text-white"> <UserAvatar diff --git a/frontend/src/metabase/admin/people/containers/UserSuccessModal.jsx b/frontend/src/metabase/admin/people/containers/UserSuccessModal.jsx index bbe6b8769be694d83c9609c0ef669b9ecd0afb24..ee65d603ba7db7f2fafd34acc1c498f2e0514f18 100644 --- a/frontend/src/metabase/admin/people/containers/UserSuccessModal.jsx +++ b/frontend/src/metabase/admin/people/containers/UserSuccessModal.jsx @@ -1,4 +1,5 @@ /* eslint-disable react/prop-types */ +import cx from "classnames"; import { Component } from "react"; import { connect } from "react-redux"; import { push } from "react-router-redux"; @@ -9,6 +10,7 @@ import ModalContent from "metabase/components/ModalContent"; import PasswordReveal from "metabase/components/PasswordReveal"; import Button from "metabase/core/components/Button"; import Link from "metabase/core/components/Link"; +import CS from "metabase/css/core/index.css"; import User from "metabase/entities/users"; import MetabaseSettings from "metabase/lib/settings"; @@ -50,7 +52,7 @@ const EmailSuccess = ({ user, isSsoEnabled }) => { )} with instructions to log in. If this user is unable to authenticate then you can ${( <Link to={`/admin/people/${user.id}/reset`} - className="link" + className={CS.link} >{t`reset their password.`}</Link> )}`}</div> ); @@ -76,7 +78,7 @@ const PasswordSuccess = ({ user, temporaryPassword }) => ( className="pt4 text-centered" > {jt`If you want to be able to send email invites, just go to the ${( - <Link to="/admin/settings/email" className="link text-bold"> + <Link to="/admin/settings/email" className={cx(CS.link, "text-bold")}> Email Settings </Link> )} page.`} diff --git a/frontend/src/metabase/admin/permissions/components/CollectionPermissionsModal/CollectionPermissionsModal.jsx b/frontend/src/metabase/admin/permissions/components/CollectionPermissionsModal/CollectionPermissionsModal.jsx index 70aa92a8a6b6b4d799965fd4fe4e959cc61880ad..58720d3231849205879693ff78ffec2e23e46a79 100644 --- a/frontend/src/metabase/admin/permissions/components/CollectionPermissionsModal/CollectionPermissionsModal.jsx +++ b/frontend/src/metabase/admin/permissions/components/CollectionPermissionsModal/CollectionPermissionsModal.jsx @@ -130,7 +130,7 @@ const CollectionPermissionsModal = ({ : [ <Link key="all-permissions" - className="link" + className={CS.link} to="/admin/permissions/collections" > {t`See all collection permissions`} diff --git a/frontend/src/metabase/admin/settings/components/UploadSettings/UploadSettings.tsx b/frontend/src/metabase/admin/settings/components/UploadSettings/UploadSettings.tsx index 9d3f91c7fd88ae719cad09546707652c41a76ab7..a3db423291d22d969a6aa9a68247af20ac8fc707 100644 --- a/frontend/src/metabase/admin/settings/components/UploadSettings/UploadSettings.tsx +++ b/frontend/src/metabase/admin/settings/components/UploadSettings/UploadSettings.tsx @@ -12,6 +12,7 @@ import Input from "metabase/core/components/Input"; import Link from "metabase/core/components/Link"; import type { SelectChangeEvent } from "metabase/core/components/Select"; import Select from "metabase/core/components/Select"; +import CS from "metabase/css/core/index.css"; import Databases from "metabase/entities/databases"; import Schemas from "metabase/entities/schemas"; import { useDispatch } from "metabase/lib/redux"; @@ -71,7 +72,7 @@ const Header = () => ( display_name: t`Allow people to upload data to Collections`, description: jt`People will be able to upload CSV files that will be stored in the ${( <Link - className="link" + className={CS.link} key="db-link" to="/admin/databases" >{t`database`}</Link> diff --git a/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing/PublicLinksListing.jsx b/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing/PublicLinksListing.jsx index 416b14b6437cb820f42144b974a5ef126b51d3e5..39a0a9a762f05901c94d3819cfe7e390cd92be08 100644 --- a/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing/PublicLinksListing.jsx +++ b/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing/PublicLinksListing.jsx @@ -100,7 +100,7 @@ class PublicLinksListing extends Component { <ExternalLink href={getPublicUrl(link)} onClick={() => this.trackEvent("Public Link Clicked")} - className="link text-wrap" + className={cx(CS.link, "text-wrap")} > {getPublicUrl(link)} </ExternalLink> diff --git a/frontend/src/metabase/admin/tasks/containers/JobInfoApp.jsx b/frontend/src/metabase/admin/tasks/containers/JobInfoApp.jsx index f7db719f188082a8099dab17d7fff6db22355352..a1410cfc27bc202579b177e88480541192a93682 100644 --- a/frontend/src/metabase/admin/tasks/containers/JobInfoApp.jsx +++ b/frontend/src/metabase/admin/tasks/containers/JobInfoApp.jsx @@ -50,7 +50,7 @@ const renderJobsTable = jobs => { <td>{job.durable}</td> <td> <Link - className="link" + className={CS.link} to={`/admin/troubleshooting/jobs/${job.key}`} > {t`View triggers`} diff --git a/frontend/src/metabase/admin/tasks/containers/TasksApp.jsx b/frontend/src/metabase/admin/tasks/containers/TasksApp.jsx index e092749b3a2448497bba5c62dbb75647313b429d..abcfb72fd645fca282f73b202447499869902bad 100644 --- a/frontend/src/metabase/admin/tasks/containers/TasksApp.jsx +++ b/frontend/src/metabase/admin/tasks/containers/TasksApp.jsx @@ -91,7 +91,7 @@ class TasksAppInner extends Component { <td>{task.duration}</td> <td> <Link - className="link text-bold" + className={cx(CS.link, "text-bold")} to={`/admin/troubleshooting/tasks/${task.id}`} >{t`View`}</Link> </td> diff --git a/frontend/src/metabase/collections/components/CollectionCopyEntityModal.jsx b/frontend/src/metabase/collections/components/CollectionCopyEntityModal.jsx index e200b0fef43da2367f6bc83756406c17a835589e..0dcd86b4ea99fe8e9bc623257eff52f8fd52dba3 100644 --- a/frontend/src/metabase/collections/components/CollectionCopyEntityModal.jsx +++ b/frontend/src/metabase/collections/components/CollectionCopyEntityModal.jsx @@ -59,7 +59,7 @@ function CollectionCopyEntityModal({ {newEntityObject.uncopied?.length > 0 ? t`Duplicated ${entityObject.model}, but couldn't duplicate some questions` : t`Duplicated ${entityObject.model}`} - <Link className="link text-bold ml1" to={newEntityUrl}> + <Link className={cx(CS.link, "text-bold ml1")} to={newEntityUrl}> {t`See it`} </Link> </div>, diff --git a/frontend/src/metabase/components/ChannelSetupMessage/ChannelSetupMessage.jsx b/frontend/src/metabase/components/ChannelSetupMessage/ChannelSetupMessage.jsx index bc83fe8585b0ec14af0b7581211ee5dff31905f3..6a2e779f8af5c1a6d8ab2c2aa984af76d3e96663 100644 --- a/frontend/src/metabase/components/ChannelSetupMessage/ChannelSetupMessage.jsx +++ b/frontend/src/metabase/components/ChannelSetupMessage/ChannelSetupMessage.jsx @@ -42,7 +42,7 @@ export default class ChannelSetupMessage extends Component { content = ( <div className="mb1"> <h4 className="text-medium">{t`Your admin's email address`}:</h4> - <a className="h2 link no-decoration" href={"mailto:" + adminEmail}> + <a className={cx("h2", CS.link)} href={"mailto:" + adminEmail}> {adminEmail} </a> </div> diff --git a/frontend/src/metabase/components/ErrorDetails/ErrorDetails.tsx b/frontend/src/metabase/components/ErrorDetails/ErrorDetails.tsx index 625a7618af4178d2de01a77f2d913685e1a66c31..56f227894c3b1a320aff5393869c0fc460acf9c4 100644 --- a/frontend/src/metabase/components/ErrorDetails/ErrorDetails.tsx +++ b/frontend/src/metabase/components/ErrorDetails/ErrorDetails.tsx @@ -2,6 +2,8 @@ import cx from "classnames"; import { useState } from "react"; import { t } from "ttag"; +import CS from "metabase/css/core/index.css"; + import { ErrorBox } from "./ErrorBox"; import type { ErrorDetailsProps } from "./types"; @@ -24,7 +26,7 @@ export default function ErrorDetails({ return ( <div className={className}> <div className={centered ? "text-centered" : "text-left"}> - <a onClick={toggleShowError} className="link cursor-pointer"> + <a onClick={toggleShowError} className={cx(CS.link)}> {showError ? t`Hide error details` : t`Show error details`} </a> </div> diff --git a/frontend/src/metabase/components/LeftNavPane/LeftNavPane.jsx b/frontend/src/metabase/components/LeftNavPane/LeftNavPane.jsx index 12565f9df6c827afbb85d58345b3598d3a982947..e777f0968e65167e4e21e295e934307baed86b43 100644 --- a/frontend/src/metabase/components/LeftNavPane/LeftNavPane.jsx +++ b/frontend/src/metabase/components/LeftNavPane/LeftNavPane.jsx @@ -54,10 +54,9 @@ export function LeftNavPaneItemBack({ path }) { AdminS.AdminListItem, CS.flex, CS.alignCenter, - CS.noDecoration, CS.textBold, CS.justifyBetween, - "link", + CS.link, )} > < {t`Back`} diff --git a/frontend/src/metabase/components/PasswordReveal/PasswordReveal.jsx b/frontend/src/metabase/components/PasswordReveal/PasswordReveal.jsx index b2c7a7faa975a11bb94bea0f64ea24b0cb07bfff..4f8a4bc80786c27e740a34289a30f8dc24b16af8 100644 --- a/frontend/src/metabase/components/PasswordReveal/PasswordReveal.jsx +++ b/frontend/src/metabase/components/PasswordReveal/PasswordReveal.jsx @@ -76,7 +76,7 @@ export default class PasswordReveal extends Component { <div className={cx(CS.mlAuto, CS.flex, CS.alignCenter)}> <a - className={cx("link", CS.textBold, CS.mr2)} + className={cx(CS.link, CS.textBold, CS.mr2)} onClick={() => this.setState({ visible: !visible })} > {visible ? t`Hide` : t`Show`} diff --git a/frontend/src/metabase/components/Triggerable/Triggerable.jsx b/frontend/src/metabase/components/Triggerable/Triggerable.jsx index d87290491ede4ca36ac20ca9da4c4c19ad4a0e02..99c677da067cde85327a36fca8b255e70273c672 100644 --- a/frontend/src/metabase/components/Triggerable/Triggerable.jsx +++ b/frontend/src/metabase/components/Triggerable/Triggerable.jsx @@ -4,6 +4,7 @@ import cx from "classnames"; import { createRef, cloneElement, Children, Component } from "react"; import Tooltip from "metabase/core/components/Tooltip"; +import CS from "metabase/css/core/index.css"; import { isObscured } from "metabase/lib/dom"; const Trigger = styled.a``; @@ -153,7 +154,7 @@ const Triggerable = ComposedComponent => triggerClasses, isOpen && triggerClassesOpen, !isOpen && triggerClassesClose, - "no-decoration", + CS.noDecoration, { "cursor-default": this.props.disabled, }, diff --git a/frontend/src/metabase/core/components/ExternalLink/ExternalLink.tsx b/frontend/src/metabase/core/components/ExternalLink/ExternalLink.tsx index 608438a43a2eba5ef160efcb54dec2b8eaf38e25..90813728595d00e8f3e40ccae0f3d770b565d39a 100644 --- a/frontend/src/metabase/core/components/ExternalLink/ExternalLink.tsx +++ b/frontend/src/metabase/core/components/ExternalLink/ExternalLink.tsx @@ -1,6 +1,7 @@ import type { AnchorHTMLAttributes, ReactNode, Ref } from "react"; import { forwardRef } from "react"; +import CS from "metabase/css/core/index.css"; import { getUrlTarget } from "metabase/lib/dom"; import { LinkRoot } from "./ExternalLink.styled"; @@ -20,7 +21,7 @@ const ExternalLink = forwardRef(function ExternalLink( <LinkRoot ref={ref} href={href} - className={className || "link"} + className={className || CS.link} target={target} // prevent malicious pages from navigating us away rel="noopener noreferrer" diff --git a/frontend/src/metabase/css/core/link.module.css b/frontend/src/metabase/css/core/link.module.css index 03bec2f0e7887b67f71fcda8378b89e4397d52ef..3495c97d7608c5dbb6bc9c69d3e433d5cba1a194 100644 --- a/frontend/src/metabase/css/core/link.module.css +++ b/frontend/src/metabase/css/core/link.module.css @@ -1,27 +1,25 @@ -:global(.no-decoration), -.no-decoration, .noDecoration { text-decoration: none; } -:global(.link) { +.link { cursor: pointer; text-decoration: none; color: var(--color-brand); } -:global(.link:hover) { +.link:hover { text-decoration: underline; } -:global(.link:focus) { +.link:focus { outline: 2px solid var(--color-focus); } -:global(.link:focus:not(:focus-visible)) { +.link:focus:not(:focus-visible) { outline: none; } -:global(.link--wrappable) { +.linkWrappable { word-break: break-all; } diff --git a/frontend/src/metabase/dashboard/containers/AutomaticDashboardApp.jsx b/frontend/src/metabase/dashboard/containers/AutomaticDashboardApp.jsx index 2a2457dcf857695b60cf9a88156ad67af6795c43..6c732e4639569c451fffc3bb926b737362e957e6 100644 --- a/frontend/src/metabase/dashboard/containers/AutomaticDashboardApp.jsx +++ b/frontend/src/metabase/dashboard/containers/AutomaticDashboardApp.jsx @@ -79,7 +79,10 @@ class AutomaticDashboardAppInner extends Component { triggerToast( <div className={cx(CS.flex, CS.alignCenter)}> {t`Your dashboard was saved`} - <Link className="link text-bold ml1" to={Urls.dashboard(newDashboard)}> + <Link + className={cx(CS.link, "text-bold ml1")} + to={Urls.dashboard(newDashboard)} + > {t`See it`} </Link> </div>, diff --git a/frontend/src/metabase/databases/components/DatabaseClientIdDescription/DatabaseClientIdDescription.tsx b/frontend/src/metabase/databases/components/DatabaseClientIdDescription/DatabaseClientIdDescription.tsx index 6948f82bbaad34fdab5340c785f7965c1c64d2da..71b2f23cadaedc08aa29367dd6b3a99959a7c372 100644 --- a/frontend/src/metabase/databases/components/DatabaseClientIdDescription/DatabaseClientIdDescription.tsx +++ b/frontend/src/metabase/databases/components/DatabaseClientIdDescription/DatabaseClientIdDescription.tsx @@ -2,6 +2,7 @@ import { useFormikContext } from "formik"; import { jt, t } from "ttag"; import ExternalLink from "metabase/core/components/ExternalLink"; +import CS from "metabase/css/core/index.css"; import type { DatabaseData } from "metabase-types/api"; const CREDENTIAL_URLS: Record<string, string> = { @@ -24,7 +25,7 @@ const DatabaseClientIdDescription = (): JSX.Element | null => { return ( <span> {jt`${( - <ExternalLink className="link" href={projectUrl.href}> + <ExternalLink className={CS.link} href={projectUrl.href}> {t`Click here`} </ExternalLink> )} to generate a Client ID and Client Secret for your project.`}{" "} diff --git a/frontend/src/metabase/lib/formatting/url.tsx b/frontend/src/metabase/lib/formatting/url.tsx index 4d9ad10b93701fb2a9e0ccb5c2280e0e74573e64..cf820fb2a86d0a815e19e927aae9179382970384 100644 --- a/frontend/src/metabase/lib/formatting/url.tsx +++ b/frontend/src/metabase/lib/formatting/url.tsx @@ -1,4 +1,7 @@ +import cx from "classnames"; + import ExternalLink from "metabase/core/components/ExternalLink"; +import CS from "metabase/css/core/index.css"; import { getDataFromClicked } from "metabase-lib/v1/parameters/utils/click-behavior"; import { isURL } from "metabase-lib/v1/types/utils/isa"; @@ -35,7 +38,7 @@ export function formatUrl(value: string, options: OptionsType = {}) { if (jsx && rich && url) { const text = getLinkText(value, options); return ( - <ExternalLink className="link link--wrappable" href={url}> + <ExternalLink className={cx(CS.link, CS.linkWrappable)} href={url}> {text} </ExternalLink> ); diff --git a/frontend/src/metabase/lib/formatting/value.tsx b/frontend/src/metabase/lib/formatting/value.tsx index 86f39280aba6604191192f9510a4afa5cbb12311..0584672d5682c183c95ef7fb5bf73e8e13bdec08 100644 --- a/frontend/src/metabase/lib/formatting/value.tsx +++ b/frontend/src/metabase/lib/formatting/value.tsx @@ -1,3 +1,4 @@ +import cx from "classnames"; import type { Moment } from "moment-timezone"; // eslint-disable-line no-restricted-imports -- deprecated usage import moment from "moment-timezone"; // eslint-disable-line no-restricted-imports -- deprecated usage import Mustache from "mustache"; @@ -5,6 +6,7 @@ import type * as React from "react"; import ReactMarkdown from "react-markdown"; import ExternalLink from "metabase/core/components/ExternalLink"; +import CS from "metabase/css/core/index.css"; import { NULL_DISPLAY_VALUE, NULL_NUMERIC_VALUE } from "metabase/lib/constants"; import { renderLinkTextForClick } from "metabase/lib/formatting/link"; import { @@ -155,7 +157,10 @@ export function formatValueRaw( // Style this like a link if we're in a jsx context. // It's not actually a link since we handle the click differently for dashboard and question targets. return ( - <div className="link link--wrappable"> + <div + data-testid="link-formatted-text" + className={cx(CS.link, CS.linkWrappable)} + > {formatValueRaw(value, { ...options, jsx: false })} </div> ); diff --git a/frontend/src/metabase/nav/components/PaymentBanner/PaymentBanner.tsx b/frontend/src/metabase/nav/components/PaymentBanner/PaymentBanner.tsx index 9ae99d6b79056850cbd0afc9ce47bec22fcec9c1..13bf516c5a5aa6d1702cd4c282338fef01c7460f 100644 --- a/frontend/src/metabase/nav/components/PaymentBanner/PaymentBanner.tsx +++ b/frontend/src/metabase/nav/components/PaymentBanner/PaymentBanner.tsx @@ -2,6 +2,7 @@ import { jt, t } from "ttag"; import Banner from "metabase/components/Banner"; import ExternalLink from "metabase/core/components/ExternalLink"; +import CS from "metabase/css/core/index.css"; import MetabaseSettings from "metabase/lib/settings"; import type { TokenStatus } from "metabase-types/api"; @@ -17,7 +18,7 @@ export const PaymentBanner = ({ isAdmin, tokenStatus }: PaymentBannerProps) => { {jt`âš ï¸ We couldn't process payment for your account. Please ${( <ExternalLink key="payment-past-due" - className="link" + className={CS.link} href={MetabaseSettings.storeUrl()} > {t`review your payment settings`} @@ -31,7 +32,7 @@ export const PaymentBanner = ({ isAdmin, tokenStatus }: PaymentBannerProps) => { {jt`âš ï¸ Pro features won’t work right now due to lack of payment. ${( <ExternalLink key="payment-unpaid" - className="link" + className={CS.link} href={MetabaseSettings.storeUrl()} > {t`Review your payment settings`} diff --git a/frontend/src/metabase/nav/components/QuestionLineage/QuestionLineage.tsx b/frontend/src/metabase/nav/components/QuestionLineage/QuestionLineage.tsx index 0dad2c89a8b8a8b876364e8780e23546cb58bcaf..afc3445313a5265945cadfd4a8c2e5219c513967 100644 --- a/frontend/src/metabase/nav/components/QuestionLineage/QuestionLineage.tsx +++ b/frontend/src/metabase/nav/components/QuestionLineage/QuestionLineage.tsx @@ -2,6 +2,7 @@ import { t } from "ttag"; import Badge from "metabase/components/Badge"; import Link from "metabase/core/components/Link/Link"; +import CS from "metabase/css/core/index.css"; import type { IconName } from "metabase/ui"; import type Question from "metabase-lib/v1/Question"; import * as ML_Urls from "metabase-lib/v1/urls"; @@ -24,7 +25,7 @@ const QuestionLineage = ({ return ( <Badge icon={icon} isSingleLine> {t`Started from`}{" "} - <Link className="link" to={ML_Urls.getUrl(originalQuestion)}> + <Link className={CS.link} to={ML_Urls.getUrl(originalQuestion)}> {originalQuestion.displayName()} </Link> </Badge> diff --git a/frontend/src/metabase/query_builder/components/AlertListPopoverContent.jsx b/frontend/src/metabase/query_builder/components/AlertListPopoverContent.jsx index 3d0179597e54f7f1c1a900120f84afab7454f091..c768d90968a83934c671bd7815c16f082504ee31 100644 --- a/frontend/src/metabase/query_builder/components/AlertListPopoverContent.jsx +++ b/frontend/src/metabase/query_builder/components/AlertListPopoverContent.jsx @@ -87,7 +87,7 @@ class AlertListPopoverContent extends Component { <div className={cx(CS.borderTop, CS.p2, "bg-light-blue")}> <a className={cx( - "link", + CS.link, CS.flex, CS.alignCenter, CS.textBold, @@ -186,11 +186,11 @@ class AlertListItemInner extends Component { }} > {(isAdmin || isCurrentUser) && ( - <a className="link" onClick={this.onEdit}>{jt`Edit`}</a> + <a className={CS.link} onClick={this.onEdit}>{jt`Edit`}</a> )} {!isAdmin && !unsubscribingProgress && ( <a - className="link ml2" + className={cx(CS.link, "ml2")} onClick={this.onUnsubscribe} >{jt`Unsubscribe`}</a> )} diff --git a/frontend/src/metabase/query_builder/components/DataSelector/DataSelectorTablePicker/DataSelectorTablePicker.tsx b/frontend/src/metabase/query_builder/components/DataSelector/DataSelectorTablePicker/DataSelectorTablePicker.tsx index b3a6b68bde43eb10a52fe44a308ad0a85401cd1b..d0afdfa88bee1c98da96cc074e3974f67981671b 100644 --- a/frontend/src/metabase/query_builder/components/DataSelector/DataSelectorTablePicker/DataSelectorTablePicker.tsx +++ b/frontend/src/metabase/query_builder/components/DataSelector/DataSelectorTablePicker/DataSelectorTablePicker.tsx @@ -1,3 +1,4 @@ +import cx from "classnames"; import type { ReactNode } from "react"; import { t } from "ttag"; @@ -7,6 +8,7 @@ import { } from "metabase/components/MetadataInfo/TableInfoIcon/TableInfoIcon"; import AccordionList from "metabase/core/components/AccordionList"; import ExternalLink from "metabase/core/components/ExternalLink"; +import CS from "metabase/css/core/index.css"; import { color } from "metabase/lib/colors"; import MetabaseSettings from "metabase/lib/settings"; import { isSyncCompleted } from "metabase/lib/syncing"; @@ -162,7 +164,7 @@ const LinkToDocsOnReferencingSavedQuestionsInQueries = () => ( "questions/native-editor/referencing-saved-questions-in-queries", )} target="_blank" - className="block link" + className={cx("block", CS.link)} > {t`Learn more about nested queries`} </ExternalLink> diff --git a/frontend/src/metabase/query_builder/components/DataSelector/TriggerComponents.tsx b/frontend/src/metabase/query_builder/components/DataSelector/TriggerComponents.tsx index 166eef94a3847e773731849fa563d06ccec2c51e..b725f9222f326bb17480f5df1edf7e32f77abe35 100644 --- a/frontend/src/metabase/query_builder/components/DataSelector/TriggerComponents.tsx +++ b/frontend/src/metabase/query_builder/components/DataSelector/TriggerComponents.tsx @@ -1,7 +1,9 @@ +import cx from "classnames"; import type { CSSProperties, ReactNode } from "react"; import { t } from "ttag"; import _ from "underscore"; +import CS from "metabase/css/core/index.css"; import { Icon, Text } from "metabase/ui"; import type Database from "metabase-lib/v1/metadata/Database"; import type Field from "metabase-lib/v1/metadata/Field"; @@ -81,22 +83,26 @@ export function FieldTrigger({ export function DatabaseTrigger({ database }: { database: Database }) { return database ? ( <span - className="text-wrap text-grey no-decoration" + className={cx("text-wrap text-grey", CS.noDecoration)} data-testid="selected-database" > {database.name} </span> ) : ( - <span className="text-medium no-decoration">{t`Select a database`}</span> + <span + className={cx("text-medium", CS.noDecoration)} + >{t`Select a database`}</span> ); } export function TableTrigger({ table }: { table: Table }) { return table ? ( - <span className="text-wrap text-grey no-decoration"> + <span className={cx("text-wrap text-grey", CS.noDecoration)}> {table.display_name || table.name} </span> ) : ( - <span className="text-medium no-decoration">{t`Select a table`}</span> + <span + className={cx("text-medium", CS.noDecoration)} + >{t`Select a table`}</span> ); } diff --git a/frontend/src/metabase/query_builder/components/ExpandableString.jsx b/frontend/src/metabase/query_builder/components/ExpandableString.jsx index ba542ce62881096946869cc0ef5d927ef2444efc..539268aed8e6d4d74733df95a54c5ab73b940c21 100644 --- a/frontend/src/metabase/query_builder/components/ExpandableString.jsx +++ b/frontend/src/metabase/query_builder/components/ExpandableString.jsx @@ -1,8 +1,11 @@ /* eslint-disable react/prop-types */ +import cx from "classnames"; import Humanize from "humanize-plus"; import { Component } from "react"; import { t } from "ttag"; +import CS from "metabase/css/core/index.css"; + export default class ExpandableString extends Component { constructor(props, context) { super(props, context); @@ -43,7 +46,7 @@ export default class ExpandableString extends Component { <span> {this.props.str}{" "} <span - className="block mt1 link" + className={cx("block mt1", CS.link)} onClick={this.toggleExpansion} >{t`View less`}</span> </span> @@ -53,7 +56,7 @@ export default class ExpandableString extends Component { <span> {truncated}{" "} <span - className="block mt1 link" + className={cx("block mt1", CS.link)} onClick={this.toggleExpansion} >{t`View more`}</span> </span> diff --git a/frontend/src/metabase/query_builder/components/VisualizationError/VisualizationError.tsx b/frontend/src/metabase/query_builder/components/VisualizationError/VisualizationError.tsx index 7f8d29b8ad89cc0d7bfbcabef552aee0789d5e98..024152ec234adc74560a5ebd15a8a51b29d55145 100644 --- a/frontend/src/metabase/query_builder/components/VisualizationError/VisualizationError.tsx +++ b/frontend/src/metabase/query_builder/components/VisualizationError/VisualizationError.tsx @@ -31,7 +31,7 @@ function EmailAdmin(): JSX.Element | null { const hasAdminEmail = isNotNull(MetabaseSettings.adminEmail()); return hasAdminEmail ? ( <span className={QueryBuilderS.QueryErrorAdminEmail}> - <a className="no-decoration" href={`mailto:${hasAdminEmail}`}> + <a className={CS.noDecoration} href={`mailto:${hasAdminEmail}`}> {hasAdminEmail} </a> </span> diff --git a/frontend/src/metabase/query_builder/components/VisualizationResult.jsx b/frontend/src/metabase/query_builder/components/VisualizationResult.jsx index 9b89e6c6af174f5e13808aef96c4969536034aea..00452d780bb46d2941a1c82f3608a37c1f80e25f 100644 --- a/frontend/src/metabase/query_builder/components/VisualizationResult.jsx +++ b/frontend/src/metabase/query_builder/components/VisualizationResult.jsx @@ -78,7 +78,7 @@ export default class VisualizationResult extends Component { <p> {jt`You can also ${( <a - className="link" + className={CS.link} key="link" onClick={this.showCreateAlertModal} > diff --git a/frontend/src/metabase/query_builder/components/template_tags/TagEditorHelp/TagEditorHelp.tsx b/frontend/src/metabase/query_builder/components/template_tags/TagEditorHelp/TagEditorHelp.tsx index 7fa023d36f88c7766864ad4d330bb50c66b78e19..6f26acc08cfa050426e24e04308cc924e0acf195 100644 --- a/frontend/src/metabase/query_builder/components/template_tags/TagEditorHelp/TagEditorHelp.tsx +++ b/frontend/src/metabase/query_builder/components/template_tags/TagEditorHelp/TagEditorHelp.tsx @@ -1,8 +1,10 @@ +import cx from "classnames"; import { t, jt } from "ttag"; import Code from "metabase/components/Code"; import Button from "metabase/core/components/Button"; import ExternalLink from "metabase/core/components/ExternalLink"; +import CS from "metabase/css/core/index.css"; import { useSelector } from "metabase/lib/redux"; import MetabaseSettings from "metabase/lib/settings"; import { uuid } from "metabase/lib/utils"; @@ -312,7 +314,7 @@ export const TagEditorHelp = ({ /> {showMetabaseLinks && ( - <p className="pt2 link"> + <p className={cx("pt2", CS.link)}> <ExternalLink href={MetabaseSettings.docsUrl( "questions/native-editor/sql-parameters", diff --git a/frontend/src/metabase/query_builder/components/template_tags/TagEditorParamParts/FilterWidgetTypeSelect.tsx b/frontend/src/metabase/query_builder/components/template_tags/TagEditorParamParts/FilterWidgetTypeSelect.tsx index 81a421fea8e05341b15bfe12d402835614cdd1b6..3c6309bb27204a8350a6d5cef799c9bc091a320f 100644 --- a/frontend/src/metabase/query_builder/components/template_tags/TagEditorParamParts/FilterWidgetTypeSelect.tsx +++ b/frontend/src/metabase/query_builder/components/template_tags/TagEditorParamParts/FilterWidgetTypeSelect.tsx @@ -2,6 +2,7 @@ import { useMemo } from "react"; import { Link } from "react-router"; import { t } from "ttag"; +import CS from "metabase/css/core/index.css"; import MetabaseSettings from "metabase/lib/settings"; import { Select } from "metabase/ui"; import type { TemplateTag } from "metabase-types/api"; @@ -65,7 +66,7 @@ export function FilterWidgetTypeSelect({ "the-field-filter-variable-type", )} target="_blank" - className="link" + className={CS.link} > {t`Learn more`} </Link> diff --git a/frontend/src/metabase/setup/components/SetupHelp/SetupHelp.tsx b/frontend/src/metabase/setup/components/SetupHelp/SetupHelp.tsx index 8286019e82ae42c1a45b16e2e0b38f5362c3ba34..6e8af10cea977171229ff3a477c024dd018a28bd 100644 --- a/frontend/src/metabase/setup/components/SetupHelp/SetupHelp.tsx +++ b/frontend/src/metabase/setup/components/SetupHelp/SetupHelp.tsx @@ -1,6 +1,7 @@ import { t } from "ttag"; import ExternalLink from "metabase/core/components/ExternalLink"; +import CS from "metabase/css/core/index.css"; import MetabaseSettings from "metabase/lib/settings"; import { SetupFooterRoot } from "./SetupHelp.styled"; @@ -10,7 +11,7 @@ export const SetupHelp = (): JSX.Element => { <SetupFooterRoot> {t`If you feel stuck`},{" "} <ExternalLink - className="link" + className={CS.link} href={MetabaseSettings.docsUrl( "configuring-metabase/setting-up-metabase", )} diff --git a/frontend/src/metabase/sharing/components/AddEditSidebar/CaveatMessage/CaveatMessage.jsx b/frontend/src/metabase/sharing/components/AddEditSidebar/CaveatMessage/CaveatMessage.jsx index 302f8647103370bd90ea031f7e84e299e8f432b2..293ebbe98746b494933e4721bfda7dc8e0312876 100644 --- a/frontend/src/metabase/sharing/components/AddEditSidebar/CaveatMessage/CaveatMessage.jsx +++ b/frontend/src/metabase/sharing/components/AddEditSidebar/CaveatMessage/CaveatMessage.jsx @@ -1,6 +1,7 @@ import { t } from "ttag"; import ExternalLink from "metabase/core/components/ExternalLink"; +import CS from "metabase/css/core/index.css"; import { useSelector } from "metabase/lib/redux"; import MetabaseSettings from "metabase/lib/settings"; import { getShowMetabaseLinks } from "metabase/selectors/whitelabel"; @@ -16,7 +17,7 @@ export function CaveatMessage() { <> <ExternalLink - className="link" + className={CS.link} target="_blank" href={MetabaseSettings.docsUrl("dashboards/subscriptions")} > diff --git a/frontend/src/metabase/sharing/components/NewPulseSidebar.tsx b/frontend/src/metabase/sharing/components/NewPulseSidebar.tsx index 3ed66590fc4d3a388a2d0090a5c37a97dcf1a8de..e6338bd7d9764470bc1ead9ffd58b9a08a9cce70 100644 --- a/frontend/src/metabase/sharing/components/NewPulseSidebar.tsx +++ b/frontend/src/metabase/sharing/components/NewPulseSidebar.tsx @@ -63,7 +63,11 @@ export function NewPulseSidebar({ > {!emailConfigured && jt`You'll need to ${( - <Link key="link" to="/admin/settings/email" className="link"> + <Link + key="link" + to="/admin/settings/email" + className={CS.link} + > {t`set up email`} </Link> )} first.`} @@ -101,7 +105,11 @@ export function NewPulseSidebar({ > {!slackConfigured && jt`First, you'll have to ${( - <Link key="link" to="/admin/settings/slack" className="link"> + <Link + key="link" + to="/admin/settings/slack" + className={CS.link} + > {t`configure Slack`} </Link> )}.`} diff --git a/frontend/test/metabase/lib/formatting.unit.spec.js b/frontend/test/metabase/lib/formatting.unit.spec.js index f00b913b5abaa7b889faa53d0676dd3c66124c8a..ec4090fd8759c9426e32a4283104e243693c5e75 100644 --- a/frontend/test/metabase/lib/formatting.unit.spec.js +++ b/frontend/test/metabase/lib/formatting.unit.spec.js @@ -268,8 +268,8 @@ describe("formatting", () => { }); // it's not actually a link expect(isElementOfType(formatted, ExternalLink)).toEqual(false); - // but it's formatted as a link - expect(formatted.props.className).toEqual("link link--wrappable"); + // expect the text to be in a div (which has link formatting) rather than ExternalLink + expect(formatted.props["data-testid"]).toEqual("link-formatted-text"); }); it("should render image", () => { const formatted = formatValue("http://metabase.com/logo.png", { @@ -498,8 +498,8 @@ describe("formatting", () => { // it is not a link set on the question level expect(isElementOfType(formatted, ExternalLink)).toEqual(false); - // it is formatted as a link cell for the dashboard level click behavior - expect(formatted.props.className).toEqual("link link--wrappable"); + // expect the text to be in a div (which has link formatting) rather than ExternalLink + expect(formatted.props["data-testid"]).toEqual("link-formatted-text"); }); });